역할과 구현으로 분리
In Java,
객체를 설계할 때 역할, 구현을 명확히 분리하고 설계 시 역할을 부여하여 역할을 수행하는 구현 객체 만든다!

1. SRP 단일 책임 원칙 (Single responsibility principle)
2. OCP 개방-폐쇄 원칙 (Open/closed principle)
/*
현재 클라이언트가 구현 클래스를 직접 선택하고 있기 때문에
새로운 구현 객체를 만들고 변경하는 과정에서 클라이언트 코드를 변경해야 한다.
*/
//private MemberRepository memberRepository = new MemoryMemberRepository();
private MemberRepository memberRepository = new JdbcMemberRepository();
-> 별도의 조립, 설정자가 필요하다! Spring이 해준다
3. LSP 리스코프 치환 원칙 (Liskov substitution principle)
4. ISP 인터페이스 분리 원칙 (Interface segregation principle)
5. DIP 의존관계 역전 원칙 (Dependency inversion principle)
객체 지향의 핵심은 다형성이지만 다형성만으로는 OCP, DIP를 지킬 수 없다. 무엇인가 더 필요하다! -> 스프링과 같은 프레임워크의 등장

1. 회원 객체
public class Member {
private Long id;
private String name;
private Grade grade;
public Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
2. 회원 서비스 인터페이스
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
3. 회원 서비스 구현 객체
public class MemberServiceImpl implements MemberService {
// 추상화에도 의존, 구현 객체에도 의존 - DIP 위반
private final MemberRepository memberRepository = new MemoryMemberRepository();
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
4. 회원 레퍼지토리 인터페이스
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
5. 회원 레퍼지토리(메모리) 구현 객체
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
6. 회원 가입 및 조회 실행
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new " + member.getName());
System.out.println("test : " + findMember.getName());
}
}
Spring framework의 어떠한 도움도 받지 않고 순수한 자바 코드로 만든 회원 도메인이다.
private final MemberRepository memberRepository = new *Memory*MemberRepository();

1. 주문 객체
public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
this.memberId = memberId;
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
public int calculatePrice(){
return itemPrice-discountPrice;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public int getItemPrice() {
return itemPrice;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
2. 주문 서비스 인터페이스
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
3. 주문 서비스 구현 객체
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@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);
}
}
4. 할인 서비스 인터페이스
public interface DiscountPolicy {
/*
return 할인 대상 금액
*/
int discount(Member member, int price);
}
5-1. 고정 할인 서비스 구현 객체
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return discountFixAmount;
}
else {
return 0;
}
}
}
5-2. 가변 할인 서비스 구현 객체
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price*discountPercent/100;
}
else{
return 0;
}
}
}
6. 주문 및 할인 실행
public class OrderApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId,"itemA",10000);
System.out.println("order + " + order);
}
}
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = **new RateDiscountPolicy();**

// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
구현 객체를 대신해서 생성하고 주입할 무엇인가 필요하다!
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
회원 서비스 구현 객체 수정(생성자 주입)
// 과거 : 추상화에도 의존, 구현 객체에도 의존 - DIP 위반
// 현재 : memoryMemberRepository에 대한 정보가 없다! -> DIP
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
회원 가입 및 조희 수정(AppConfig 사용)
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new " + member.getName());
System.out.println("test : " + findMember.getName());
}
}
주문 및 할인 서비스 구현 객체 또한 똑같이 수정했다.

할인 정책 변경(고정 -> 가변)
public DiscountPolicy discountPolicy(){
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
DIP, OCP 위반 문제가 해결되었다!
AppConfig처럼 객체를 생성하고 의존관계를 연결해주는 것을 IoC, DI 컨테이너라고 한다
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
/*AppConfig appConfig = new AppConfig();
// 수동으로 자바 객체 생성
MemberService memberService = appConfig.memberService();*/
// 스프링 컨테이너가 자동으로 객체 생생허새 스프링 Bean으로 관리
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// getBean(메소드이름, 반환 클래스)
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
1. 스프링 컨테이너 생성

// AnnotationConfigApplicationContext(구성 정보);
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
2. 스프링 빈 등록

3.스프링 빈 의존관계 설정 - 준비

4.스프링 빈 의존관계 설정 - 완료

@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
//1. 조회 : 호출할 때마다 객체 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회 : 호출할 때마다 객체 생성
MemberService memberService2 = appConfig.memberService();
//참조값이 다른걸 확인
System.out.println("m1 "+memberService1);
System.out.println("m2 "+memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
public class SingletonService {
// static 영역에 객체 딱 1개 생성
private static final SingletonService instance = new SingletonService();
// 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회
public static SingletonService getInstance(){
return instance;
}
// 외부에서 new 키워드에서 객체 생성 불가
private SingletonService(){
}
}
@Test
@DisplayName("싱글톤 패턴 적용 ")
void singletonServiceTest(){
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("s1 "+ singletonService1);
System.out.println("s2 "+ singletonService2);
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
}
@Test
@DisplayName("스프링 컨테이너")
void springContainer(){
// AppConfig를 기반으로 스프링 컨테이너 생성
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//조회
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
//조회
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
System.out.println("m1 "+memberService1);
System.out.println("m2 "+memberService2);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
public class StatefulService {
private int price;
public void order(String name, int price){
System.out.println(name + " " + price);
this.price = price;
}
public int getPrice(){
return price;
}
}
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// A
statefulService1.order("userA",10000);
// B
statefulService2.order("nameB",20000);
// A??
int price = statefulService1.getPrice();
System.out.println("price = "+price);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
@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();
}
}
@Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberService -> memberRepositoy "+memberRepository1);
System.out.println("orderService -> memberRepositoy "+memberRepository2);
System.out.println("memberRepositoy "+memberRepository);
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
Config 파일에서 기존에는 @Bean 어노테이션을 활용해서 수동으로 스프링 빈을 생성하고 의존관계를 주입했다. 하지만 스프링은 @Component, @ComponentScan을 통해 스프링 빈을 자동 생성하고 @Autowired를 통해 의존관계도 자동으로 주입해주는 아주 똑똑한 놈이다.
기존 Config 파일
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
System.out.println("AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
컴포넌트 스캔 활용
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
@Component
public class RateDiscountPolicy implements DiscountPolicy
@Component
public class MemoryMemberRepository implements MemberRepository
@Component
public class MemberServiceImpl implements MemberService
@Component
public class OrderServiceImpl implements OrderService
Autowired 의존관계 자동 주입
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@Autowired
private DiscountPolicy discountPolicy
@Autowired
private DiscountPolicy rateDiscountPolicy
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap );
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
스프링 빈의 이벤트 라이프 사이클
객체의 생성과 초기화 분리
생성자는 필수 정보(파라미터)를 통해 메모리를 할당하고 객체를 생성하는 책임을 갖고 있다. 생성자 안에 무거운 초기화 작업을 함께 하는 것보다 객체 생성, 초기화를 명확하게 구분하는 것이 좋다!
public class Network implements InitializingBean, DisposableBean{
@Override
public void afterPropertiesSet() throws Exception{
// 초기화 기능
}
@Override
public void destory() throws Exception{
// 소멸 전 기능
}
}
public class NetworkClient {
public void init() {
// 초기화 기능
}
public void close() {
// 소멸 전 기능
}
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
public class NetworkClient {
@PostConstruct
public void init() {
// 초기화 기능
}
@PreDestory
public void close() {
// 소멸 전 기능
}
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
@Test
void prototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
System.out.println("sing1 = " + bean1);
System.out.println("sing2 = " + bean2);
Assertions.assertThat(bean1).isNotSameAs(bean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy");
}
}

@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class,ClientBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
Assertions.assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
Assertions.assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
@Component
@RequiredArgsConstructor
static class ClientBean{
private final PrototypeBean provider;
public int logic(){
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@Scope("prototype")
@Component
static class PrototypeBean{
private int count = 0;
public void addCount(){
count++;
}
public int getCount(){
return count;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy");
}
}
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
static class ClientBean {
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
웹 스코프 종류
예제
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message){
System.out.println("["+uuid+"]"+"["+requestURL+"]"+message);
}
@PostConstruct
public void init(){
uuid = UUID.randomUUID().toString();
System.out.println("["+uuid+"] request scope bean create: "+ this);
}
@PreDestroy
public void close(){
// System.out.println("MyLogger.close");
System.out.println("["+uuid+"] request scope bean close: "+ this);
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerPrvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURI().toString();
MyLogger myLogger = myLoggerProvider.getObject();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testID");
return "OK";
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURI().toString();
System.out.println("myLogger = " + myLogger.getClass());
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testID");
return "OK";
}
}