성장일기

내가 보려고 정리하는 공부기록

백엔드/스프링

[Spring] 1. 순수 자바로 설계하기 - 스프링 핵심원리 기본편

와나나나 2024. 1. 29. 12:58
728x90

인프런 김영한 강사님의 스프링 핵심원리 강의를 듣고 정리하고자 한다.

프로그램을 만들 때, 요구사항을 보고 설계를 하듯이 이 강의도 요구사항을 읽고 설계하여 구현하는 방식으로 수업이 진행된다.  순서는 대략 아래와 같이 진행되었다.

 

  1. 요구사항 보고 대략 구조짜기
  2. 자바로 구현하기
  3. Spring 입히기

해당 게시글에서는 순수 자바로 구현하는 것까지만 작성해 둘 예정이다.


1. 요구사항 보고 구조짜기 & 다이어그램 그리기

요구사항을 읽어보고 확정 되지 않은 조건은 인터페이스만 설계해 둔다! 추후에 구현하면 되기 때문에 인터페이스만 잘 설계해도 괜찮다. 수업에서 진행한 설계에서는 회원데이터가 자체DB를 구축할지 외부 시스템을 연동할지 확정이 나지 않은 상황이었기 때문에 회원 저장소를 인터페이스로 설계해두고 자체DB와 외부시스템을 구현한다.

 

전체적인 틀을 다이어그램으로 그려보았다. 다이어그램을 그리면 어떤 기능을 어느 클래스에 넣어야할지, 어떤 인터페이스가 필요할지 한눈에 들어오게 만들 수 있다. 프로젝트를 할 때 필요한 과정이라고 생각했다.

 

짜고 나서는 큼직한 도메인을 하나씩 자바로 구현한다.

 

 

2. 자바로 구현하기

회원도메인 개발을 위해 회원, 회원서비스, 회원 저장소를 구현한다. 회원 저장소를 구현할 때 강의에서는 간단하게 HashMap을 사용했으나, 동시성 이슈가 발생할 수 있어 ConcurrentHashMap을 사용해아 한다고 한다.

 


+ HashMap과 ConcurrentHashMap

HashMap은 Map 역할을 해주는 클래스로 정말 많이 쓰인다. 

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    public V get(Object key) {}
    public V put(K key, V value) {}
}

 

해시맵 클래스에는 synchronized 키워드가 붙어있지 않다는 걸 알 수 있는데, 이는 멀테스레드 환경에서 사용이 힘들다는 의미이다. 대신 성능은 정말 좋다! 때문에 멀티 스레드 환경이 아니면 HashMap을 쓰는것이 좋으나, 멀티스레드에서는 사용할 수 없다. 이를 개선한 것이 ConcurrentHashMap이다.

 

ConcurrentHashMap은 멀티스레드 환경에서 사용할 수 없는 HashMap의 단점을 보완한 클래스이다. 

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

    public V get(Object key) {}

    public boolean containsKey(Object key) { }

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    ...

}

 

concurrentHashMap은 버킷 단위로 lock을 사용하고, 버킷 개수가 16개로 설정되어있다. 즉, 16개의 스레드를 동시에 돌릴 수 있다는 의미이다. 이런 방식을 사용함으로써 같은 버킷을 사용하지 않는 이상 lock을 기다릴 필요가 없다.

 


 

해시맵에 대한 설명은 이정도로 하고 다시 본론으로 돌아오면, 결론은 ConcurrentHashMap을 사용하라는 것이다. 이렇게 쭉 구현을 하다가 memberService 구현을 하는데, 이때는 회원저장소를 불러와야한다.

package hello.core.member;

public class MemberServiceImpl implements MemberService {
	
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    public void join(Member member) {
    	memberRepository.save(member);
    }
    
    ....
    
}

 

 private final MemberRepository memberRepository = new MemoryMemberRepository() 부분을 보면 앞 부분은 인터페이스에 의존하고 뒷부분은 구현체에 의존하는 것을 볼 수 있다. 이는 DIP를 위반하는 것이다!!

 

이 문제는 는 주문 도메인의할인 정책 적용에도 일어난다.

public class OrderServiceImpl implements OrderService {
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
     private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    
}

  

위 코드는 인터페이스와 구현체에 동시 의존할 뿐만 아니라 rate정책을 쓰냐 fix정책을 쓰냐에 따라 코드를 수정해야 한다.

구현체에도 의존하고 있다는 점에서 DIP 위반, 코드를 변경해야 한다는 점에서 OCP 위반이 일어난다.

 

 

우선 DIP위반을 해결하려면, 인터페이스에만 의존하도록 해야한다! 그럼 코드를 아래처럼 수정해볼 수 있지 않을까?

public class OrderServiceImpl implements OrderService {
	// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
     private DiscountPolicy discountPolicy;
    
}

 

그런데 이러면 구현체가 없어서 코드 실행이 안 된다. 이를 해결하려면 구현 객체를 대신 생성하고 주입할 누군가가 필요하다는 뜻이다. 여기서 관심사의 분리가 필요하다. 이전 게시물에서 이야기 했듯이 하나의 개체는 하나의 책임만 맡아야한다. 여기서 AppConfig가 나온다.

 

AppConfig

AppConfig는 구현 객체를생성하고 연결하는 책임을 갖는 별도의 클래스를 만든 것이다! 여기에서 실제 동작에 필요한 구현 객체를 생성해주면 되고, 생성자를 통해 주입시키면 된다.

package hello.core.member;

public class MemberServiceImpl implements MemberService {
	
    private final MemberRepository memberRepository;
    
    public MemberServiceImpl (MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }    
    ....
    
}

 

이렇게 생성자를 통해 주입시켜준다! 이러면 DIP를 지켜낼 수 있다.

 

이렇게 마치 외부에서 주입해주는 것처럼 설계된 것을 DI (Dependency Injection) 의존관계주입 이라고 한다.

AppConfig에는 역할과 구현이 잘 드러나게 코드를 작성하면 된다.

 

package hello.core;

// import 생략

public class AppConfig {

	public MemberService memberService() {
    	return new MemberServiceImpl(memberRepository());
    }
    
    public OrderService orderService() {
    	return new OrderServiceImpl(
        	memberRepository(),
            discountPolicy());
    }
    
    ppublic MemberRepository memberRepository() {
    	return new MemoryMemberRepository();
    }
    
    public DiscountPolicy discountPolicy() {
    	return new FixDiscountPolicyy();
    }

}

 

이렇게 AppConfig에서만 코드를 변경해주면 되고, 사용 영역의 코드는변경할 필요가 없어진다.

 

이렇게 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI컨테이너라고 한다.