포스트

2022-03-06-TIL

Today I Learned

JPA vs MyBatis

JPA는 객체와 테이블을 매핑해 두고 엔티티를 조작하면 SQL이 생성되는 ORM 표준이고, MyBatis는 개발자가 쓴 SQL의 파라미터와 결과를 객체에 매핑해 주는 SQL 매퍼다. JPA는 단순한 CRUD와 도메인 모델 중심 개발에서 반복 코드를 크게 줄여 주지만, 영속성 컨텍스트, 지연 로딩, N+1 같은 동작 원리를 알아야 의도한 쿼리가 나간다. MyBatis는 실행되는 SQL이 눈에 그대로 보여 복잡한 통계 쿼리나 기존 DB 스키마에 맞추기 쉽지만, 테이블이 바뀌면 관련 SQL과 매핑을 모두 손봐야 한다. 한 프로젝트에서 기본은 JPA로 두고 복잡한 조회만 QueryDSL이나 MyBatis로 처리하는 혼용도 흔하다.


Package Structure Domain

패키지를 controller, service, repository처럼 계층별로 나누는 대신 member, order처럼 기능(도메인)별로 나누자는 주장이다. 링크된 글은 기능별 패키지가 관련 코드를 한곳에 모아 응집도를 높이고, 기능 하나를 고칠 때 여러 패키지를 오가지 않아도 되며, 패키지 단위로 접근 제어(package-private)를 활용할 수 있다는 점을 장점으로 든다. DDD에서도 바운디드 컨텍스트나 애그리거트 단위로 패키지를 나누는 방식과 잘 맞는다.

1
2
3
4
5
6
계층별                    기능별
├── controller            ├── member
│   ├── MemberController  │   ├── MemberController
│   └── OrderController   │   ├── MemberService
├── service               │   └── MemberRepository
...                       └── order ...
  • https://hackernoon.com/package-by-features-not-layers-2d076df1964d
  • https://stackoverflow.com/questions/55245747/should-we-use-package-by-feature-structure-with-ddd

같은 테이블에 대한 엔티티가 여러개인 경우

하나의 테이블에 대해서 여러개의 엔티티가 존재하여 참조할 경우, 동시성 문제가 없는가?

Thorben Janssen의 글에 따르면 같은 테이블에 엔티티 두 개를 매핑하는 것은 가능하다. 공통 속성을 @MappedSuperclass로 빼고, 자주 쓰는 컬럼만 가진 엔티티와 전체 컬럼을 가진 엔티티가 각각 @Table(name = "author")로 같은 테이블을 가리키게 하면 된다. 다만 Hibernate는 두 엔티티가 같은 행을 나타낸다는 사실을 모른다. 그래서 다음 문제가 생긴다.

  • 한쪽 엔티티를 수정해도 다른 쪽 엔티티는 갱신되지 않는다.
  • 한 엔티티 타입을 find로 조회해도 다른 엔티티 타입의 대기 중인 변경은 flush되지 않아 오래된 값을 읽을 수 있다.
  • 같은 트랜잭션에서 같은 행을 두 엔티티로 각각 수정하면 @Version이 있을 때 OptimisticLockException이 난다.

글은 두 엔티티 모두로 쓰기 작업을 하거나 연관관계를 정의해야 할 때만 이렇게 하고, 조회만 필요하면 DTO 프로젝션을 쓰라고 권한다. 한쪽을 읽기 전용(@Immutable)으로 만드는 것도 방법이다.

  • https://thorben-janssen.com/hibernate-tips-map-multiple-entities-same-table/#:~:text=If%20you%20want%20to%20map,is%20not%20an%20entity%20itself.
  • https://stackoverflow.com/questions/18547932/hibernate-one-table-multiple-entities

JUnit asssertTrue vs AssertJ assertThat

assertTrue(list.contains("a"))는 실패했을 때 “expected true but was false” 정도만 알려 준다. AssertJ의 assertThat(list).contains("a")는 실제 리스트 내용과 기대값을 함께 보여 주므로 실패 원인을 바로 알 수 있고, 메서드 체이닝으로 읽는 순서가 문장처럼 자연스럽다. IDE 자동 완성으로 타입별 검증 메서드를 찾을 수 있다는 것도 장점이다.

1
2
assertTrue(name.startsWith("Kim") && name.length() < 10);   // 실패 이유가 안 보인다
assertThat(name).startsWith("Kim").hasSizeLessThan(10);     // 어느 조건이 깨졌는지 보인다
  • https://www.reddit.com/r/java/comments/cdwsyk/junit_asserttrue_vs_assertthat_readability/
  • https://jongmin92.github.io/2020/03/31/Java/use-assertthat/

Spring Rest Docs

Swagger가 너무 코드를 복잡하게 만든다면 Spring Rest Docs를 고려해볼 수 있다.

Swagger(springfox 등)는 컨트롤러에 @ApiOperation 같은 애너테이션을 붙여 문서를 만들기 때문에 프로덕션 코드에 문서용 코드가 섞인다. Spring REST Docs는 반대로 테스트에서 문서를 만든다. Spring 가이드처럼 MockMvc 테스트에 document("home")을 추가하면 테스트가 실행될 때 HTTP 요청, 응답, curl 예시 등의 Asciidoctor 스니펫이 생성되고, 이를 묶어 HTML 문서로 만든다. 요청/응답 필드를 문서화하도록 지정하면 실제 응답과 문서가 어긋날 때 테스트가 실패하므로 문서가 코드와 함께 유지된다. 대신 테스트를 작성해야 문서가 생기고, Swagger UI처럼 바로 호출해 보는 화면은 기본으로 없다.

  • https://spring.io/guides/gs/testing-restdocs/
  • https://techblog.woowahan.com/2597/
참고한 자료외부 출처 2

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다