Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- Java
- 티스토리챌린지
- 국비지원취업
- baekjoon
- 디자인챌린지
- UXUI챌린지
- 내일배움캠프
- 디자인교육
- 내일배움카드
- 디자인강의
- 환급챌린지
- 국비지원교육
- 백엔드
- 객체지향
- 부트캠프
- Be
- 백엔드 부트캠프
- 패스트캠퍼스
- OPENPATH
- 오픈챌린지
- 국비지원
- KDT
- 백준
- 오블완
- Spring
- UXUIPrimary
- 오픈패스
- mysql
- 백엔드개발자
- UXUI기초정복
Archives
- Today
- Total
군만두의 IT 공부 일지
[스터디2] 07. 도메인 및 알아두면 유용한 스프링 활용법 본문
목차
10. 도메인
- 소프트웨어 공학에서 도메인: 애플리케이션이 해결하고자 하는 문제 영역
10.1 소프트웨어 개발의 시작
린(lean) 방식의 업무 스타일
1. 사용자의 문제 상황을 인식함.
2. 문제 상황에 따라 어떤 솔루션을 제공하면 좋은 반응을 얻을 것이라고 가설을 세움.
3. 가설이 맞다면 결과가 어떤 지표로 반영될 것이라고 가정함.
4. 가설을 검증할 수 있는 가장 빠른 방법을 생각하고 이를 실험함.
5. 사용자와 지속적으로 소통하면서 가설의 방향성을 지속적으로 조정, 확장함.
- 린 방식의 업무 스타일에서는 사용자가 겪는 문제와 사용자의 문제를 해결할 수 있는 해결책을 만들어야 함을 강조함.
- 오늘날 대부분의 사업은 고객의 문제에서 출발함. 여기서 사용자들이 겪는 문제 영역이 바로 도메인임.
10.2 애플리케이션의 본질
- 애플리케이션에서 도메인을 제외한 다른 부분은 도메인을 해결하기 위한 도구이자 수단일 뿐임.
- 중요한 것은 사용자에게 어떤 형태로 비즈니스 가치를 전달할 수 있는가, 그리고 그러기 위한 도메인 분석과 도메인 설계임.
10.3 도메인 모델과 영속성 객체
*도메인 모델: 애플리케이션의 핵심 비즈니스 로직과 규칙을 표현하는 객체
*영속성 객체(Persistence Object): 데이터를 데이터베이스에 저장하거나 불러오는 역할을 담당하는 객체
도메인 모델과 영속성 객체를 구분하는 방식(구분하기 전략):
역할에 따라 도메인 모델과 영속성 객체를 나눔. '계정'이라는 모델이 있을 때, 도메인 모델로서 존재하는 Account 클래스와 데이터베이스 영속화를 담당하는 영속성 객체인 AccountJpaEntity 클래스를 분리해서 관리하겠다는 의미임.
도메인 모델과 영속성 객체를 하나의 통합된 클래스로 관리하는 방식(통합하기 전략):
도메인 모델과 영속성 객체를 하나의 클래스로 관리함. Account 클래스 하나에 두 객체의 역할이 들어갈 수 있게 함. 여기서 Account 클래스는 도메인 모델이면서 동시에 영속성 객체임. 프로젝트가 JPA를 사용하고 있다면 @Entity 애너테이션을 Account 클래스에 등록해서 사용하고 있음을 의미함.
어떤 전략이 더 나은 전략일지 비교 분석하기 위해서 다음과 같은 요구사항을 가정한다.
- 계정(account)이라는 도메인이 있음.
- 계정은 닉네임을 변경할 수 있음.
- 계정은 데이터베이스에 저장되고 불러올 수도 있어야 함.
10.3.1 통합하기 전략
// 통합하기 전략을 사용해 Account 객체를 표현
@Data
@Entity(name = "account")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
@Column
private String nickname;
public void changeNickname(String nickname) {
this.nickname = nickname;
}
}
- 장점: 구분하기 전략에 비해 개발 속도가 빠름. 하나의 클래스만 잘 관리하면 비용 문제가 발생하지 않음. JPA의 역할이 ORM(객체-관계 매핑)이라는 점에서 자연스러움.
- 단점: 클래스의 책임이 제대로 눈에 들어오지 않음. 도메인이 커질수록 모델을 관리하기 어려움. 단일 책임 원칙을 위반함.
10.3.2 구분하기 전략
// 구분하기 전략을 사용해 도메인 객체를 표현
@Builder
public class Account {
private Long id;
private String email;
private String nickname;
public void changeNickname(String nickname) { // 이 메서드는 도메인에 요구되는 요구사항이므로 도메인 로직임.
this.nickname = nickname;
}
}
// 구분하기 전략을 사용해 영속성 객체를 표현
@Data
@Entity(name = "account")
public class AccountJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
@Column
private String nickname;
public static AccountJpaEntity from(Account account) {
AccountJpaEntity result = new AccountJpaEntity();
result.id = account.getId();
result.email = account.getEmail();
result.nickname = account.getNickname();
return result;
}
public Account toModel() {
return Account.builder()
.id(this.id)
.email(this.email)
.nickname(this.nickname)
.build();
}
}
- 장점: 도메인이 ORM 같은 특정 라이브러리에 의존하지 않음. 애플리케이션이 관계형 데이터베이스에 의존하지 않음. 즉, 도메인의 책임과 데이터 영속의 책임을 구분해 유연함.
- 단점: 유사한 모델을 두번 만들어야 함. 영속성 객체를 도메인 모델로 매핑하는 메서드도 추가로 필요함. ORM의 혜택을 누리기 어려움.
10.3.3 평가
통합하기 전략:
- 도메인과 영속성 객체를 통합함.
- 작성해야 하는 코드의 양이 줄어듦.
- 도메인이 영속성 라이브러리에 강결합됨.
구분하기 전략:
- 도메인과 영속성 객체를 분리함.
- 작성해야 하는 코드의 양이 늘어남.
- 도메인과 영속성 라이브러리가 분리됨.
- 결론: '통합하기 전략'이 '구분하기 전략'과 비교해서 항상 부정적인 결과를 만들지는 않음. 두 전략 중 어느 전략이 옳고 그른지 확정할 수 없음.
- 개인적으로는 아래와 같이 사용하는 것이 좋다고 생각함.
- 소규모 프로젝트: 통합하기 전략 → 빠른 개발과 유지보수를 중점으로 하기 때문.
- 대규모 프로젝트: 구분하기 전략 → 확장성과 유지보수성을 중점으로 하기 때문.
11. 알아두면 유용한 스프링 활용법
11.1 타입 기반 주입
- 스프링의 @Autowired을 이용한 의존성 주입은 타입을 기반으로 빈을 찾아 주입함. 해당하는 빈을 찾지 못하면 NoSuchBeanDefinitionException 에러를 던짐.
- 추상 타입을 주입할 때, 이를 상속하거나 구현하는 빈이 여러 개일 경우 스프링은 NoUniqueBeanDefinitionException 에러를 던짐.
- 주입할 빈이 여러 개일 경우 @Qualifier나 @Primary를 사용하여 해결할 수 있음.
- @Qualifier: 주입하려는 빈을 지정함.
- @Primary: 타입이 일치하는 빈이 여러 개일 때 특정 빈을 가장 우선해서 주입하게 함.
- List<추상타입>을 주입받으면, 타입과 일치하는 모든 빈을 찾아 List에 추가함.
- OCP 위반을 타입 기반 주입으로 해결한 예제
- 문제점: 새로운 푸시 알림 채널 추가 시
- NotificationType에 새로운 타입 추가
- NotificationService의 notify 메서드에 새로운 case 블록 추가 → OCP 위반
- 새로운 푸시 알림 컴포넌트 추가
- 해결: 타입 기반 주입과 인터페이스 기반 설계
- NotificationChannel 인터페이스를 만들어 알림 채널의 공통 동작 정의
- supports 메서드를 통해 각 채널이 지원하는 타입을 명확히 구분
- 각 알림 채널(Email, Push 등)은 NotificationChannel을 구현해 독립적으로 동작
- List<NotificationChannel> 타입 기반 주입으로 새로운 채널 추가 시 기존 코드 수정 불필요 → OCP 준수
- 문제점: 새로운 푸시 알림 채널 추가 시
//switch가 아닌 for 문으로 멤버 변수를 순회하도록 변경
// NotificationService 컴포넌트 코드를 수정하지 않고 푸시 알림을 지원할 수 있음.
@Service
@RequiredArgsConstructor
public class NotificationService {
private final List<NotificationChannel> notificationChannels;
public void notify(Account account, String message) {
for (NotificationChannel notificationChannel : notificationChannels) {
if (notificationChannel.supports(account)) {
notificationChannel.notify(account, message);
}
}
}
}
// NotificationChannel에 supports 메서드 추가
public interface NotificationChannel {
boolean supports(Account account);
void notify(Account account, String message);
}
// 기존 컴포넌트들도 isSupport를 지원하도록 변경
@Component
public class EmailNotificationChannel implements NotificationChannel {
@Override
public boolean supports(Account account) {
return account.getNotificationType() == NotificationType.EMAIL;
}
@Override
public void notify(Account account, String message) {
// account에 해당하는 message를 이메일로 전송함.
}
}
// 새로운 요구사항을 처리하기 위해 만들어진 푸시 알림 컴포넌트
@Component
public class PushNotificationChannel implements NotificationChannel {
@Override
public boolean supports(Account account) {
return account.getNotificationType() == NotificationType.PUSH;
}
@Override
public void notify(Account account, String message) {
// account에 해당하는 message를 푸시 알림으로 전송함.
}
}
- 정리: 시스템이 푸시 알림을 위한 채널을 새롭게 지원해야 한다고 가정하면, 개발자가 할 일은 푸시 알림용 컴포넌트를 만들고, 이를 스프링 빈으로 등록하기만 하면 됨. 코드 수정을 최소화하여 시스템의 동작을 확장할 수 있게 됨.
- 스프링의 타입 기반 주입을 활용하면 SOLID에서 OCP를 프레임워크 수준에서도 적용할 수 있음.
11.2 자가 호출
- 자가 호출(self invocation): 어떤 객체가 메서드를 처리하는 중에 자신이 갖고 있는 다른 메서드를 호출하는 상황
- 스프링의 AOP는 프록시를 기반으로 동작함. 자가 호출이 발생하면, 프록시가 아닌 본래 객체(this)가 호출됨. 따라서 호출되는 메서드에 적용된 AOP 애너테이션(@Transactional 등)이 동작하지 않을 수 있음.
// 스프링을 사용할 때 발생할 수 있는 자가 호출
@Controller
@RequiredArgsConstructor
class MyController {
private final MyService myService;
@GetMapping
@ResponseStatus(OK)
public Object doSomething() {
myService.doSomething1();
return null;
}
}
@Service
@RequiredArgsConstructor
class MyService {
public void doSomething1() {
doSomething2(); // 자가 호출: 같은 클래스 내에서 호출되기 때문에 @Transactional이 적용되지 않을 수 있음.
}
@Transactional
public void doSomething2() {
// do something...
}
}
타입 기반 주입 예에 대해서 책에서 설명한 것처럼, 저도 기능을 확장하려고 서비스 코드를 직접 수정했던 경험이 떠올라 뜨끔했습니다. 앞으로 스프링의 유용한 기능들을 더 잘 활용할 수 있도록 체계적으로 공부해야겠습니다.
이 글은 『자바/스프링 개발자를 위한 실용주의 프로그래밍』 책을 학습한 내용을 정리한 것입니다.
'프로그래밍 > 객체지향' 카테고리의 다른 글
[스터디2] 09. 테스트 대역 (0) | 2025.02.08 |
---|---|
[스터디2] 08. 자동 테스트 및 테스트 피라미드 (0) | 2025.01.31 |
[스터디2] 06. 모듈 (0) | 2025.01.15 |
[스터디2] 05. 레이어드 아키텍처 (0) | 2025.01.13 |
[스터디2] 04. 안티패턴 및 서비스 (1) | 2025.01.04 |
Comments