Post

[DDD] Factory Method & Repository

[DDD] Factory Method & Repository

Factory method

“객체를 어떻게 만드는지” 보다 “왜/언제 만들어 지는지” 를 드러내고 복잡한 생성과정을 감추는 역할

Factory method의 장점

  • 생성 로직이 복잡한 경우
    • new User(username, email, password, …) 같이 끝나지 않고 암호화, 중복체크, 값 객체 조립 같이 여러 단계가 필요할때
  • 도메인 용어로 의도를 드러내고 싶을 때
    • 생성자는 단순히 “객체를 만든다” 인데, 팩토리메서드는 회원등록(register), 예약생성(reserve), 주문발행(placeOrder) 같이 도메인행위를 들어낼 수 있음 = 읽는 사람을 편하게 읽히게 해줌!
  • 여러 생성 방법이 있을 때
    • 같은 엔티티라도 다르게 초기화가 필요할 수 있음
    • 생성자 오버로딩 대신 팩토리메서드로 구분하면 명확해짐
  • 생성자 노출을 제한하고 싶을 때
    • 생성자를 protectedprivate으로 막아두고, 반드시 팩토리 메서드를 통해서만 객체가 만들어지도록 강제.
    • 잘못된 방식으로 객체가 만들어지는걸 방지

Repository 사용

User.java (엔티티)

1
2
3
4
5
6
7
8
9
public static User register(String username, String email, String encryptedPassword, String nickname, String phone) {
        return new User(
            username,
            email,
            encryptedPassword,
            nickname,
            phone
        );
    }

Service 계층

1
2
3
4
5
6
7
8
9
10
@Override
public void registerUser(UserRegistrationRequest request) {
    userValidator.RegisterValidate(request.username(), request.email());

    String encoded = passwordEncoder.encode(request.password());

    User user = User.register(request.username(), request.email(), encoded, request.nickname(), request.phone());

    userRepository.save(user);
}

new를 사용했을 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void registerUser(UserRegisterRequest request) {
    //잘못된 사용 가능성: 암호화를 빼먹을 수 있음
    //왜? 생성하는지 보는사람이 모를 수 있음
    User user = new User(
        request.username(),
        request.email(),
        request.getPassword(), // 평문 들어감 (보안 문제)
        request.nickname(),
        request.phone()
    );

    userRepository.save(user);
}

생성자는 객체 내부 구현 세부사항을 드러내는 반면, 팩토리 메서드는 도메인 언어로 행위를 드러내어 모델을 더 읽기 쉽게 만든다.


Repository

도메인 객체를 컬렉션처럼 다루게 해주고, DB 세부사항은 감추게 해준다.

Repository의 장점

  • 도메인 객체 저장/수정 이 필요할때
    • User, Order 같은 엔티티를 DB에 저장하거나 불러와야 할 때
    • 도메인 계층은 DB를 몰라도되고 그냥 컬렉션에 넣고 꺼낸다는 느낌으로 사용
  • 도메인 계층의 순수성 유지
    • 엔티티가 SQL, JPA 같은 기술에 오염되지 않도록
    • 서비스, 도메인 계층은 User를 저장한다. 라고만 표현
    • 구체적인 DB 접근은 Spring Data JPA 구현체가 알아서
  • 의도를 보여주는 쿼리메서드
    • findByEmail(String email) 같은 메서드 시그니처 자체가 도메인 규칙을 드러냄
  • 테스트 용이
    • 리포지토리를 인터페이스로 두면, 테스트에서 가짜 구현(Mock, InMemoryRepo 등) 으로 갈아 끼울 수 있음

규격서 개정 DAO → Repository 리펙토링

기존 개정 로직

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public SpcfShtMVo rvsnSpcfsht(SpcfShtMVo vo) {
    // 원본 규격서 / 규격서 최종 규격서 여부 N 처리
    updateOriginalSpcfSht(vo);
  
    /*********************저장 METHOD***********************/
    if(!this.spcfShtValidCheck(vo)){
      throw new CustomException("해당 공정으로 등록된 품목 규격서가 존재합니다.");
    }
  
    // 개정 규격서 등록 SELECT INSERT , 개정 규격서 일련 번호 반환
    SpcfShtMVo rvsnSpcfshtVo = createNewSpcfSht(vo);
    vo.setRvsnSpcfshtSn(rvsnSpcfshtVo.getSpcfshtSn()); // 개정 규격서 일련번호
    vo.setOriginSpcfshtSn(rvsnSpcfshtVo.getOriginSpcfshtSn()); // 기존 규격서 일련번호
  
    // 개정 규격서 / 품목채취정보 SELECT INSERT
    createNewItemColctInfo(vo);
  
    // 개정 규격서 / 품목별 규격서 이력 저장
    saveReversionSpcfshtHstry(vo);
  
    // 개정 규격서 / 품목채취정보 SELECT INSERT
    spcfShtMDao.insertNewItemColctInfo(vo);
  
    // 개정 규격서 / 품목모니터링Point SELECT INSERT
    createNewItemMonitrgPoint(vo);
  
    // 개정 규격서 / 규격서-시험항목 및 변수 SELECT INSERT
    createNewTestArtcls(vo);
  
    // 개정된 규격서 조회
    return spcfShtMDao.selectSpcfsht(vo);
}
  • DAO를 직접 호출해 SQL에 의존
  • 데이터 접근 로직과 비지니스 로직이 혼합 → 비지니스 로직이 DB작업 나열로 보임
  • insertNewItemColctInfo→ 구체적 구현에 의존

도메인 엔티티

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
@Table(name = "SY_ITEM_MONITRG_POINT")
public class SpcfShtMVo {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
		...
    private String wkroomAtPointSn;
    private String spcfshtSn;
    private String bplcCd;
    
    private boolean last; // 최종 규격 여부
    public SpcfShtMVo revise() {
        this.last = false;
        return new SpcfShtMVo(/* 필요한 값 복사/초기화 */);
    }
}

레파지토리 인터페이스

1
2
3
4
5
6
@Repository
public interface SpecificationSheetRepository extends JpaRepository<SpcfShtMVo, Long> {
    Optional<SpcfShtMVo> findById(Long id);
    
    //커스텀 쿼리도 정의 가능
}

서비스 레이어

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
public SpcfShtMVo reviseSpecification(Long sheetId) {
    SpcfShtMVo original = repository.findById(sheetId)
        .orElseThrow(() -> new NotFoundException("규격서 없음"));

    SpcfShtMVo revised = original.revise();

    repository.save(original);
    repository.save(revised);

    return revised;
}
  • 서비스 코드는 ‘저장’이라는 비즈니스 의도만 남아 코드량 줄어 → 가독성 🔺
  • DB 세부사항은 Repository가 담당 → 책임 분리
  • 테스트 시 InMemoryRepository 대체 가능 → 단위 테스트 쉬움
  • revise()도메인 의미가 코드에 드러남 → 의사소통 용이
  • 로직 변경 시 엔티티 내부만 수정하면 됨 → 유지보수성 🔺

DAO는 데이터 접근 세부사항을 드러내는 반면, 리포지토리는 도메인 언어로 의도를 드러내어 모델을 더 순수하게 만든다.



「도메인 주도 설계란 무엇인가? 쉽고 간략하게 이해하는 DDD」를 읽고

This post is licensed under CC BY 4.0 by the author.