![[공부 시간 기록/개발일지] (2) DDD? (내 방식대로 해보기)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrhMQy%2FbtsNbokQ7tR%2FeoMYx8pUfdZakApJqyxkj1%2Fimg.jpg)

1. DDD를 왜 사용하냐구?

뭔가 나는 나중에 이 프로젝트를 가지고 내가 공부한걸 적용시켜보고싶다 Jpa도 연결해보고 mybatis도 써보고 이런식으로 여러개를 레고블럭 끼웠다 빼듯이 사용할수 있는 아키텍처 구조를 사용하고 싶다.
📌Domain Layer
- model
핵심 객체가 들어감 domain에서 가장 중요한 녀석 - repository
도메인 입장에서의 저장소 추상화. 저장소를 추상화 했기때문에 저장소가 달라져도 구현체만 변경하면 된다.
📌Application Layer
- service
도메인 로직을 조합하고 유스케이스를 실행하는 계층 인터페이스라서 구현체만 변경하는 방식
📌Infrastructure Layer
- external
외부 시스템과 통신하는 곳. Oauth나 controller 혹은 이벤트 발행등이 여기에 위치한다. - persistence
저장소를 구현하는 곳. Domain Layer에 있는 repository를 구현한다. - service
service로직의 구현체를 만드는 곳
이렇게 만듬으로써 service로직에서는 domain안에 추상된 repository를 바라봐 persistence가 어떻게 생겼든지 종속성이 없어졌으며 external도 추상된 service로 직을 바라봄으로써 service 구현이 어떤식으로 되던지 종속성이 없어진다.
즉 각 계층에서 다른계층이 어떻게 생기든 신경을 안써도 된다.
2. 이러다 보니 생긴 문제
코드 부터 보자
@Getter
@AllArgsConstructor
public class WakaToken {
private final Long userId;
private final String wakaId;
private final String accessToken;
private final String refreshToken;
private final String tokenType;
private final String scope;
private final LocalDateTime expiresAt;
}
@Getter
@AllArgsConstructor
public class WakaTokenDto {
private final Long userId;
private final String wakaId;
private final String accessToken;
private final String refreshToken;
private final String tokenType;
private final String scope;
private final LocalDateTime expiresAt;
@Entity
@Table(name = "user_waka_token")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class WakaTokenEntity {
@Id
private Long userId;
private String wakaId;
private String accessToken;
private String refreshToken;
private LocalDateTime expiresAt;
private String scope;
private String tokenType;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
문제점이 뭔거 같은가?
- 같은 method가 생길 확률이 높다.
Dto <-> Domain <-> Entity 서로 변환을 시작할테고 이들을 toEntity, toDomain등을 사용할 것이다.
추가적인 Domain이 생길때마다 그들의 객체에 메서드를 만든다? 이건 너무 비효율적이다. - 중복 필드가 많다.
userId등 wakaId등 3개의 객체가 같은 필드를 가지고있는데 다른 곳에 있다보니 필트가 사라진다거나하면 3곳에서 전부 변경해줘야한다.
4. 해결방법?
나의 프로젝트 DDD 방식으로는 아래 3가지 방식으로 객체를 생성할 것이다.
- Domain (Ex : WakaToken)
말그대로 domain. 이를 기준으로 해서 나머지 Entity 혹은 dto가 정해진다. Entity 혹은 dto는 service나 repository를 어떻게 구현하냐에 따라서 달라지기 때문에 domain을 기준으로 한다. - Entity (Ex : WakaTokenEntity)
지금 상태로는 jpa에서 사용하기 위한 객체이다. domain을 추종하며 DB에 저장되는 방식 혹은 DB에서 가져오는 방식을 위해서 생성한다. - Dto (Ex : WakaTokenDto)
Controller혹은 다른 외부 통신에 사용할 dto
이를 토대로 설명하려한다.
1. 같은 method를 방지하는 방법
abstract
public abstract class Convertible<T> {
@SuppressWarnings("unchecked")
public T convertTo(Class<T> targetType) {
try {
T target = targetType.getDeclaredConstructor().newInstance();
BeanUtils.copyProperties(this, target);
return target;
} catch (Exception e) {
throw new RuntimeException("객체 변환 실패: " + targetType.getSimpleName(), e);
}
}
}
이를 각각의 객체들이 상속받는 방식이다.
//상속을 받는다.
public class WakaTokenEntity extends Convertible<WakaToken>
public class WakaTokenDto extends Convertible<WakaToken>
public class WakaToken extends Convertible<WakaToken>
WakaToken token = wakaTokenDto.convertTo(WakaToken.class);
WakaTokenDto dto = token.convertTo(WakaTokenDto.class);
WakaTokenEntity entity = token.convertTo(WakaTokenEntity.class);
WakaToken token2 = entity.convertTo(WakaToken.class);
이런식으로 객체들이 class를 지정해서 변환하는 방식이다.
Java 객체 변환 방식 비교
항목 | 직접 매핑 | BeanUtils | ModelMapper | MapStruct |
🔧 변환 방식 | 수동 getter/setter/Builder 호출 | 리플렉션 + getter/setter | 리플렉션 + 내부 매핑 트리 분석 | 컴파일 타임 코드 생성 (초고속) |
⚡ 속도 | 🥇 최고 (최대 성능) | 🥈 빠름 | ❌ 느림 (리플렉션 반복) | 🥈 거의 직접 매핑급 빠름 |
🧠 사용 편의성 | ❌ 노가다 | ✅ 간단 | ✅ 매우 쉬움 | ⭕ 한 번 설정하면 자동 |
🔁 중첩 객체 매핑 | ❌ 직접 처리 | ❌ 직접 처리 | ✅ 자동 | ✅ 자동 처리 가능 |
🧪 타입 안전성 | ✅ 완전 | ❌ 없음 (런타임 오류) | ❌ 없음 (런타임 오류) | ✅ 컴파일 오류로 검출 가능 |
🛠 커스터마이징 | ✅ 코드로 가능 | ❌ 거의 불가능 | ✅ 메서드/매핑 커스터마이징 쉬움 | ✅ 어노테이션 기반 세부 매핑 가능 |
📦 의존성 필요 | ❌ 없음 | ❌ 없음 | ✅ 필요 (modelmapper) | ✅ 필요 (mapstruct, processor) |
💬 실무 적합도 | 작은 프로젝트 / 성능 중요할 때 | 단순 복사용 | 편의성 우선 개발에 좋음 | 대규모, 유지보수 필요한 시스템에 최고 |
gpt를 통해서 조사를 해봤다. 그럼 장단점도 비교해보자
- 직접 매핑 (협업을 할때 편하지 않을까? 고정된 것이 없으니까)
장점 : 의존성이 필요없고 속도가 빠름으로 좋을것 같음 또한 안전할거 같음
단점 : 중복된 코드가 많이 생길 가능성이 있음 또한 필드가 변경될시 많은 에러를 볼수있을꺼 같음 - BeanUtils (단점이 너무큼)
장점 : 상속을 통해서 빠르게 변환작업을 해줄수 있다.
단점 : 타입 안정성이 없다 그래서 런타임 에러가 난다. 또한 객체안에 객체가 있을경우 깊은 복사가 안된다. - ModelMapper (느리다는게 가장 단점)
장점 : 빠르게 사용가능하고 한번만 정의해두면 된다. 깊은 복사까지 가능하다.
단점 : 속도가 느리다. 도한 의존성이 필요하다. - MapStruct (의존성을 추가해야되는 것이 단점)
장점 : 빠르게 사용가능하며 매핑급으로 빠르다.
단점 : 의존성을 추가해야하는 것이 단점 그리고 협업할때 추가 학습및 규칙이 필요하지 않을까
그래서 나는? Mapper를 사용하지 않고 직접 매핑 코딩하려고한다.
왜? 열심히 조사했자나
DDD식으로 설계하고 있지 않은가?
만약에 내가 mapper을 변경한다면 외부에 의해서 유연하게 대응하지 못한다.
예를들어서 jpa을 사용하다가 고객사 혹은 천재지변으로 세상에 jpa가 사라져 다른거를 써야한다고하면 변경해야하는것이 많아진다.
mapper로 인해 헥사고날/DDD에서 의도한 Persistence 교체 가능성이 깨지는 것이다.
그래서 BaseEntity랑 BaseDto만 만들고 service로직으로 들어가려면 무조건 domain으로 변환해서 들어가는 식으로 코드를 만들겠다.
즉
- Controller 혹은 외부 통신 어딘가 : DTO 사용
- Service 구현체 : Domain 사용
- Repository : Entity
요런식으로 코드를 작성하려고 한다.
2. 같은 필드 를 방지하는 방법
@Getter
@Setter
@MappedSuperclass // JPA에서 필드 인식하게 하려면 필요 (Entity에 상속할 경우)
public abstract class TokenFields {
protected String accessToken;
protected String refreshToken;
protected String tokenType;
protected String scope;
protected LocalDateTime expiresAt;
}
Token 상속전용 필드를 만든다. 여기서 허점이 있으니 찾아보자 DDD를 사용하는 의의를 부정하는 부분이 있다.
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "user_waka_token")
public class WakaTokenEntity extends TokenFields {
@Id
private Long userId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@Getter
@Setter
@NoArgsConstructor
public class WakaTokenDto extends TokenFields {
private Long userId;
}
@Getter
@Setter
@NoArgsConstructor
public class WakaToken extends TokenFields {
private Long userId;
}
이렇게 나머지 domain, entity, dto가 상속을 받는다.
허점을 찾았는가?
바로 abstract class에서 @MappedSuperclass를 사용함으로써 JPA에 종속된다는 것이다. 나중에 Test를 진행할 때 javax.persistence의 의존성이 따라오게 되고 DDD를 사용하는 이유를 제거해 버린다.
그렇기 때문에 이 방식을 사용하지 않을꺼 같다.
5. 결론
같은 필드 같은 메서드를 상속하는 걸 방지하는 생각을 했는데 DDD의 종속성 제거에 대해서 더 생각해 보게 된거 같다.
위와 같은 설계로 내가 기대하는 건 다음과같다.
- DDD, SRP(단일 책임 원칙)
의의 : 각 계층이 자기 역할에만 집중하고 다른 계층의 세부 구현에 의존하지 않는다.
설계 : Domain, Entity, Dto의 사용처가 명확함 - Hexagonal
의의 : 외부 시스템들을 Adapter 패턴을 통해서 분리하는 것
설계 : Application, Infrastructure, External로 계층을 분리하며 추상화를 통해서 구현 - OCP (개방/페쇄 원칙)
의의 : 새로운 기능을 추가해도 기존 코드는 변경하지 않도록 폐쇄하는것
설계 : infrastructure에서 추상화를 통해 뭔가가 변경되도 다른 계층에서 변경할 필요가 없다, - 의존성 역전 원칙
의의 : 상위 계층 (Application)은 하위 계층 (Infrastructure)의 구현이 아닌 추상에 의존한다.
설계 : domainRepository를 인터페이스로 두고, JPA/Mongo/MyBatis 등을 구현체로 연결 할 수 있다. - POJO
의의 : 순수 Java 객체를 유지하려는 방향성
설계 : TokenDomain을 JPA에 종속되지 않게 설계하려는 고민 - 코드 재사용성과 DRY 원칙 및 추상화
의의 : 공통 동작을 추상 클래스에 위임
설계 : BaseEntity, BaseDto
사실이들을 기대한건 아니다. 그냥 유지보수를 어떻게 하면 쉽게 바꿀수 있을까 고민하다 보니까 이렇게 만들어진거 같다. 외부 api를 사용해서 인증을 해야하다 보니 나중에 뭔가 변경될 수 있다는 생각에 좀더 깊게 생각해본거 같다.
Coding, Software, Computer Science 내가 공부한 것들 잘 이해했는지, 설명할 수 있는지 적는 공간