개발 구조 기획 단계
1. 요구사항 분석
- 사용자 흐름 (User Flow)
- 핵심 기능 (로그인, 글쓰기, 댓글 등)
- 주요 시나리오 (어떤 상황에서 어떤 동작이 일어나는가)
2. 유스케이스 정의
- 각 기능을 행동 중심으로 명세화
- 예: "회원은 이메일과 비밀번호로 가입할 수 있다" → POST /signup
3. 도메인 모델 설계 (개념적)
- 어떤 객체들이 존재하는가 (User, Post, Order 등)
- 어떤 관계를 가지는가 (User 1:N Post)
4. ERD 작성
- 위에서 도출된 도메인 모델 기반으로
- 실제 DB 테이블 구조화
5. API 명세서 작성
- 유스케이스에 맞춰 API 라우팅과 요청/응답 형식 정리
- 이때 Postman이나 Swagger 등으로 샘플도 만들어두면 좋아
6. 클래스 다이어그램/서비스 흐름 설계
- Controller-→Service→Repository 흐름 정리
- 코드 기반으로 바뀌는 마지막 설계 단계
7. 코드 작성 시작
OCP, DIP 위반 이슈
- 주문 기능을 수행하는 OrderServiceImpl이 추상화와 구현체 둘 다에 의존하게 되며, DIP을 위반하게 됨.
- 또한, 기존의 FixDiscountPolicy에서 할인 정책을 변경하기 위해 새로운 구현체인 RateDiscountPolicy를 넣어주는 것으로 코드를 변경함으로써 OCP도 위반하게 됨.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository= new MemoryMemberRepository();
// OrderServiceImpl이 추상화인 DiscountPolicy와 구현체인 FixDiscountPolicy 둘 다에 의존함.
// 그래서 추상화에만 의존한 것이 아니라 DIP 위배한 것임.
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// 새로운 할인 정책을 적용하기 위해 코드를 수정하게 되면서, OCP 위반.
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
따라서 추상화에만 의존하도록(선언만) 코드를 작성하고, 구현체 주입은 스프링에서 하도록(IoC) 한다.
OCP, DIP 위반 이슈 해결 -> AppConfig(IoC 컨테이너 or DI 컨테이너)
- AppConfig 클래스를 통해, 클래스에 필요한 구현체를 생성자를 통해 주입해준다(-> DI). 즉, 역할과 구현을 철저히 분리함으로써 DIP를 지키는 것.
- 애플리케이션이 어떻게 동작할지에 대한 전체 구성을 책임지는 것이다. 이러한 AppConfig를 IoC 컨테이너 혹은 DI 컨테이너라고 부른다.
- 덕분에 의존관계에 대한 책임은 외부(AppConfig)에 맡기고 실행에만 집중할 수 있게 된다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
// -------------------AppConfig-------------------------------
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
// ---------------AppConfig 리펙토링--------------------------------
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}

- 또한, AppConfig에서 주입해주는 구현 객체를 다르게 해주면서 OCP도 지킬 수 있다.
- 할인가를 바꾸기 위해서, AppConfig에서 주입해주는 DiscountPolicy 구현 객체를 바꿔서 반환해주면 된다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}

스프링으로 바꾸기
- @Bean이라고 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록함.
- 스프링 컨테이너(ApplicationContext)를 통해 등록된 빈을 가져올 수 있도록 한다.
- DI 컨테이너 AppConfig과 메서드에 어노테이션을 추가해준다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
// ---------------------------------------------------------
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// 빈을 관리하는 스프링 컨테이너
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 메서드 이름으로 등록된 빈을 찾음
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "MemberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("Member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}'공부 > Spring' 카테고리의 다른 글
| [스프링 핵심 원리 4 / 인프런 김영한] 싱글톤 컨테이너 (0) | 2025.03.22 |
|---|---|
| [스프링 핵심 원리 3 / 인프런 김영한] 스프링 컨테이너와 스프링 빈 (0) | 2025.03.22 |
| [스프링 핵심 원리 1 / 인프런 김영한] 객체 지향 설계와 스프링 (0) | 2025.03.20 |
| [스프링 입문 5] AOP (0) | 2025.03.15 |
| [스프링 입문 4] 스프링 DB 접근 (0) | 2025.03.15 |
