NEXTSTEP ATDD 총 정리 - 6주간의 기록

gibeom·2023년 3월 24일
0

멘토링

목록 보기
15/15
post-thumbnail

아래의 링크는 NEXTSTEP ATDD 과정을 모두 정리해놓은 Github 주소입니다. 필요한 분들을 한번씩 참고해보세요 :)


더욱 개운하고 뿌듯했던 6주간의 여정

2023년도 새로운 해가 시작된 1월부터 시작된 NEXTSTEP의 ATDD 과정이 3월 말이 돼서야 모두 완주했다. 코드숨을 듣고 NEXTSTEP을 바로 듣게 된 계기는 향로 멘토님의 추천이었지만, 개인적으로 필요하다고 느꼈던 수강 목적은 다음과 같다.

  • 복잡한 비즈니스 로직에서도 코드를 깔끔하게 유지하는 역량을 키우고 싶었다.
  • Mock 없는 테스트를 통해 테스트의 신뢰도를 높일 수 있는 전략을 학습하고 싶었다.
  • 블랙박스 테스트를 통해 레거시 코드를 안전하고 효율적으로 리팩토링하는 역량을 연습하고 싶었다.

사실 테스트 관련 멘토링을 또 듣는 것을 약간 망설인 시기도 있었지만, 과정을 수료하고 위의 3가지 목적을 모두 달성한 것 같아서 정말 잘 들었다고 생각했다.

일단 가장 찜찜했었던 부분이 테스트 코드 관련 부분이었는데, 이번 과정을 통해 나만의 테스트 전략을 확실하게 정하게 된 계기가 되었다.
관련된 내용은 NEXTSTEP 5주차 - 테스트 리팩토링, 그 속으로에서 처음으로 나오는 “멀고 험난한 리팩터링, 안전하게라도 하자”라는 문단에서 작성해놓았다.

또한 블랙박스 테스트의 보호 속에서 복잡한 비즈니스 로직을 어떻게 읽기 쉬운 코드로 리팩토링하는지를 많이 연습해 본 경험이었다.

지금까지 작성했던 NEXTSTEP의 각 주차 회고에 첫 주제들은 항상 내가 느낀 점들을 작성한 후, 학습한 내용들을 적어 내려갔다. 하지만 강의에 모든 내용을 적지는 않았으며, 강의 자료를 그대로 작성하기보다는 내가 따로 추가 학습하면서 이해했던 생각의 흐름(?)대로 써보았다.
실제로 강사님은 내가 정리해놓은 내용들 그 이상으로 되게 많은 꿀팁들과 자료들을 주셨다. 하지만 저작권(?)의 문제도 있을 것이며, 그런 꿀팁들을 내가 아직 완벽하게 누구한테 설명할 정도로 습득하진 않은 상태라서 말을 아꼈다.

앞으로 개발을 계속해나가면서 배운 내용들을 체화하면 그때 나의 관점에서 열심히 작성해서 공유해볼 생각이다.


피드백과 문제 해결을 통한 또 한번의 성장

이제 코드숨 총 정리때 처럼 피드백을 받은 내용과 문제를 분석하면서 추가로 학습한 내용들을 모두 정리해보며 다시 한번 리마인드하는 시간을 가져보려고 한다.

피드백

1. import문을 통해 presentation 계층과 application 계층의 단방향 의존을 확인하자 (관련 리뷰)

  • import 문을 통해 패키지, 객체 간 의존을 확인하자
  • 의존성을 단방향으로 관리하는 방법 (Trade- off)
    • application 계층에 DTO를 두어 Service 메서드 결과를 해당 DTO로 반환해주는 방법
      • 이렇게 되면 컨트롤러 메서드와 1:1 매핑이 돼서 다른 곳에서 Service 메서드를 재사용할 수 없는 단점
    • application 계층에서 도메인 엔티티를 그대로 반환해주는 방법
      • 이렇게 되면 재사용은 가능하지만 트랜잭션을 웹 계층까지 물고가는 단점

2. 일급 컬렉션을 사용하자 (관련 리뷰)

  • 일급 컬렉션은 하나의 컬렉션을 상태로 가지고, 그 상태만을 위한 행위들을 제공하는 커스텀 자료구조이다
  • 컬렉션의 final은 재할당만 금지하고, 불변을 만들어주지는 않는다.
  • 컬렉션의 값을 직접 변경할 수 있는 메서드(ex. getter)를 제공해주지 말자

3. RequestDto에 단일 매개변수 생성자만 존재할 경우 데이터 바인딩(역직렬화)에 실패한다 (관련 리뷰)

  • @JsonCreator을 사용하거나 기본 생성자를 같이 생성해놓으면 된다.
    • @JsonCreator : 기본 생성자 대신 지정해준 생성자를 가지고 역직렬화를 진행하라고 알려주는 역할
  • Spring 내부에서 사용하는 Jackson 라이브러리를 통해 직렬화&역직렬화 시 ObjectMapper를 사용한다.
    • 이 때 jackson-module-parameter-names 모듈을 통해 기본 생성자가 없어도 인자가 있는 생성자를 통해 데이터를 바인딩해줌
    • 하지만 인자가 한개인 생성자만 있는 경우에는 HttpMessageNotReadableException이 발생하며 요청 데이터를 제대로 역직렬화 하지 못함

4. 일급 컬렉션에서의 디미터 법칙 (관련 리뷰)

[디미터 법칙에 대해 오해하고 있었던 부분을 다시 한번 되짚었던 계기]

A라는 한 엔티티 객체에서 @Embedded를 통해 일급 컬렉션 객체를 가지고 있었다. 그 일급 컬렉션 객체의 메서드를 사용하기 위해 Service 로직에서는 A.get일급컬렉션객체().메서드()를 사용하였다.

머릿속에서는 관련 데이터를 가지고 있는 객체에 직접 변경 요청을 하라는 Tell, Don't Ask 원칙이 생각났기 때문이다. 하지만 이렇게 A 객체의 내부 구현에 직접적으로 의존한다면 추후에 리팩토링을 할 때 문제가 생길 수 있다는 걸 알게되었다.

따라서 외부 모듈에서 일급 컬렉션의 메서드를 사용할 때는 A 객체의 메서드를 통해서만 메시지를 주고받도록 하고, 생성한 A 객체의 메서드는 그대로 일급컬렉션 메서드로 위임하도록 하였다.

  • Don't Talk to Strangers (모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다)
  • 디미터 법칙을 준수하는 방법 4가지
    1. 객체 자기 자신의 메서드 사용

    2. 메서드의 파라미터로 넘겨받은 객체의 메서드 사용

    3. 메서드 내부에서 생성 및 초기화한 인스턴스(객체)의 메서드 사용

    4. 해당 객체가 인스턴스 변수로 가지고 있는 객체의 메서드 사용
      (나는 위의 사례를 해당 방식으로 수정하였다)

      // 디미터 법칙을 준수하는 4가지 방법 예시
      public class Member {
          private Gender gender;
      
          public void addMember(Member member) {
          }
      
          public void updateMember(Age age) {
              age.get(); // 2. 메서드의 파라미터로 넘겨받은 객체의 메서드 사용 (O)
          }
      
          public void Demeter_법칙을_잘지키기() {
              addMember(new Member()); // 1. 객체 자기 자신의 메서드 사용 (O)
      
              Name name = new Name("ALEX");
              name.getFullName(); // 3. 메서드 내부에서 생성 및 초기화한 객체의 메서드 사용 (O)
      
              gender.get(); // 4. 인스턴스 변수로 가지고 있는 객체가 소유한 메서드 사용 (O)
          }
      }

5. Entity vs VO vs DTO (관련 리뷰)

  • Entity : 식별자 동등성을 따라 pk가 같다면 동등한 객체로 판단
    • pk((id)가 다르고 나머지 멤버가 모두 같다 하더라도 동등하다고 판단하지 않음
    • 고유한 식별자(id)가 있음
    • 엔티티는 항상 변경 가능하다.
  • VO(값 객체) : 구조적 동등성을 따라 객체의 모든 멤버가 일치할 때 동등한 객체로 판단
    • 고유한 식별자(id)가 없음
    • VO는 항상 하나 또는 여러개의 엔티티에 속해야 하며 단독으로 존재할 수 없다
    • 또한 값 객체는 불변이어야 한다
  • https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences/

6. public & private 메서드 추상화 및 위치 (관련 리뷰1, 리뷰2, 리뷰 3)

  • public 메서드 작성 시 적절하게 작업들을 추상화하여 private 메서드로 분리하여 가독성을 증가시키는 것이 좋음
  • 코드를 추상화할 때 상위 레벨에서 도메인 로직을 얼마나 잘 표현하는지가 중요
    1. 도메인 로직을 너무 심하게 추상화하여 세부 사항이 오히려 덜 드러나진 않은지 체크
    2. 또한 메서드 내부 코드의 추상화 레벨이 서로 동일한지, 다른지 체크
  • 위의 방법대로 private 메서드를 통해 추상화를 진행한 후, 관련 로직들을 순서대로 배치하여 위에서 아래로 메서드들이 읽힐 수 있게 배치하는 것이 좋음
  • 또한 하나의 public 메서드에 너무 많은 private 메서드로 추상화한다면 역할이 너무 많을 수도 있다는 것을 확인해봐야함

7. 테스트에 필요한 데이터를 공통적으로 세팅해주는 방법 (관련 리뷰)

  • ApplicationListener<ContextRefreshedEvent>를 구현(implement)하여 onApplicationEvent()메서드에 데이터를 생성하는 로직 호출
  • 각 테스트마다 @BeforeEach메서드에 데이터를 생성하는 로직 호출
  • 테스트 격리 방법에 따라 사용하는 방법을 결정하면 될 듯 함

8. application 계층의 Service 메서드 관리 (관련 리뷰)

  • OSIV를 끄는 방식 vs OSIV를 키는 방식
    • Spring Boot의 osiv는 기본적으로 활성화되어있다.
  • OSIV를 끄는 방식
    • 영속성 컨텍스트의 생존 범위는 트랜잭션 범위에서만 유효
    • 트랜잭션이 종료되는 시점(Service 메서드에서의 반환)에서 해당 엔티티 객체 멤버는 참조가 가능하지만, 객체 그래프 탐색은 불가능함(LazyInitializationException)
    • 따라서 Service 메서드에서 Entity를 그대로 반환하지 않고, 필요한 DTO를 생성해서 데이터를 옮겨담아서 반환함
    • 이렇게 되면 하나의 Controller에 의존되기에 다른 곳에서 Service 메서드 재사용이 불가능함
  • OSIV를 키는 방식
    • 영속성 컨텍스트의 생존 범위는 요청부터 응답까지 전 범위에서 유효
    • 하지만 Spring OSIV에서 트랜잭션 범위에서 트랜잭션 범위 밖에서는 영속 상태이지만 수정은 불가능하다 (즉 모든 변경은 트랜잭션 안에서만 이루어져야함)
    • 따라서 트랜잭션 없이 읽기(Nontransactional reads)가 가능하므로 Service 메서드에서 Entity를 그대로 반환하고, Controller에서 응답 DTO로 변환하여 반환
    • 이렇게 되면 Service 메서드를 다양한 Controller에서 재사용할 수 있는 장점이 있음
    • 하지만 Presentation 계층에서 지연로딩에 의한 SQL이 실행되므로, 성능 튜닝 시 관리 포인트가 넓어지는 단점이 있고, 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점도 주의해야된다
  • 실무에서는 보통 OSIV를 끄는 방식으로 많이 진행되고 있는 것 같다.
    만약 하나의 Service에 중복되는 메서드가 있다고 생각이 들면, 해당 Service 객체에서 갖고있어야 되는 로직이 맞는지 한번 고려해보면 좋을 것 같다.

9. 엔티티 직접 참조 vs 간접 참조 (관련 리뷰)

  • 엔티티를 직접 참조한 경우
    • 객체간의 결합도가 높아져 의도치 않은 동작이 일어날 수 있음
    • 다른 객체로 탐색이 가능해짐 (객체 그래프 탐색)
      • 잘 쓰면 편한 만큼 잘 못 쓰면 리스크가 있음 (의도치 않은 사이드 이펙트)
    • 디비에서 조회하거나 ORM 사용 시 복잡도가 중가됨
  • 엔티티를 간접 참조한 경우
    • 필요한 관계의 객체 id만 가지므로 객체간 결합도는 낮아져 사이드 이펙트의 확률은 줄어듬
    • 하지만 특정 요청에서 생애주기가 겹칠 경우 비즈니스 로직보다 애플리케이션의 로직이 더 커질 수 있는 단점
  • 같은 생애주기로 관리될 경우에는 직접 참조가 아주 유용하고, 같이 사용되는 빈도가 낮은 관계의 객체들은 간접 참조가 좀 더 유리할 수 있다.

10. 변수명과 메서드명에 type을 이용하는 명명하는 방법을 지양하자 (관련 리뷰)

  • 리스트를 반환하는 의미로 getxxxxList()를 사용하면 추후에 반환 타입 변경이 일어날 시 수정 포인트가 늘어날 수 있음
  • 따라서 type을 사용하기보다는 복수형으로 사용할 것을 권장
    • getFavoriteList()favorites()

문제 해결 + 기타 학습 내용

1. CascadeType.REMOVE vs orphanRemoval = true

  • CasecadeType.REMOVEorphanRemoval = true 모두 부모 엔티티를 삭제하면 자식 엔티티도 삭제한다.
    • 따라서 여러 부모 엔티티와 관계를 맺는 자식 엔티티에는 사용을 조심해야 한다
  • 하지만 부모 엔티티를 삭제하는것이 아닌, 부모 엔티티에서 자식 엔티티를 제거(관계 끊기)할 경우는 동작이 다르다
    • CasecadeType.REMOVE : 부모 엔티티에서 자식 엔티티의 관계를 제거하더라도 DELETE 문이 나가지 않는다.
    • orphanRemoval = true : 부모 엔티티에서 자식 엔티티의 관계를 제거하면 자식은 고아로 취급되어 그대로 사라진다 (DELETE 발생)
  • https://tecoble.techcourse.co.kr/post/2021-08-15-jpa-cascadetype-remove-vs-orphanremoval-true/

2. @ElementCollection & @CollectionTable

  • 엔티티의 일부 속성을 컬렉션으로 매핑할 때 사용

  • 값 타입의 라이프 사이클은 엔티티를 따라간다

    • 값 타입은 독립적인 라이프 사이클을 가질 수 없다
    • 따라서 별도로 persist()를 해주지 않아도 엔티티의 값이 변경되면 알아서 자동 반영된다
    • 영속성 전이 + 고아객체 기능을 기본적으로 가져간다
  • CollectionTable

    • name : 값 타입을 저장할 별도의 테이블 명

    • joinColumns : 참조할 fk 컬럼명 (보통 소속된 엔티티의 pk값)

      @ElementCollection(fetch = FetchType.EAGER)
      @CollectionTable(
              name = "MEMBER_ROLE", // 값 타입 용 별도 테이블 명
              joinColumns = @JoinColumn(name = "id", referencedColumnName = "id") // id랑 fk
      )
      @Column(name = "role")
      private List<String> roles;
  • 생성되는 테이블 관계



3. 설정 파일 (properties vs yml)

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html

  • properties와 yml 설정 파일이 모두 같은 위치에 있는 경우 properties 파일이 우선권을 갖는다
  • application-{profile}.yml 형식으로 로드하려고 시도한다
  • placeholder({xxxx})의 속성 이름은 kebab-case(항상 소문자만 사용)를 사용하여 참조해야 Spring Boot가 relaxed binding 기능을 사용해서 센스 있게 프로퍼티 이름을 자동으로 변환해줄 수 있음
    • @Value@ConfigurationProperties로 설정값을 바인딩 할 때
    • kebab-case : demo.item-price O | demo.itemPrice X
    • Relaxed Binding: 값을 바인딩할 때 몇가지 완화된 규칙을 사용 - 관련 다큐먼트 링크
      • 예를 들어, @Value("${demo.item-price}")는 설정 파일에서 demo.item-pricedemo.itemPrice 양식과 시스템 환경의 DEMO_ITEMPRICE를 가져옵니다.
        대신 @Value("${demo.itemPrice}")를 사용하면 demo.item-priceDEMO_ITEMPRICE가 고려되지 않습니다.

4. 프로퍼티의 키 값과 placeholder 값이 같으면 예기치 못한 동작이 발생한다

  • key={$key} 형태가 되면 안됨 → db.url=${db.url} : 오류
  • 해당 프로퍼티 값은 프로퍼티 파일, 시스템 환경 변수 또는 command-line 인자 등과 같은 다양한 방법으로 정의될 수 있음
  • 만약 프로퍼티 키와 값이 같다면, 프로퍼티 값(db.url)을 해석하려고 할 때, 그 값은 동일한 키와 연관된 값인 ${db.url}로 대체
    • db.url의 속성 값을 확인하려고 할 때 ${db.url}을 동일한 키와 연결된 값인 ${db.url}로 대체 (db.url ←→ ${db.url} : 무한 루프 발생)
profile
꾸준함의 가치를 향해 📈

1개의 댓글

comment-user-thumbnail
2023년 6월 23일

메일 보냈는데 시간 괜찮으실 때 확인부탁드릴게요!

답글 달기