개발 구조 기획 단계

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();
    }
}

주입 객체 변경으로 OCP 지키기

 

 

스프링으로 바꾸기

  • @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());
    }
}