티스토리 뷰

2022.04 ~ 2022.05 진행
   - 김영한 님의 인프런 스프링 핵심 원리 강의 정리
   - 스프링의 본질과 객체 지향의 필요성을 느끼게 되는 강의였다.

1일차 - 스프링과 객체지향설계 시작

  • 스프링 핵심
    - 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
    - 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크
  • 스프링 vs 스프링부트 차이
    - 스프링은 톰캣 설정해야 하는 번거로움이 있지만 스프링 부트는 톰캣이 웹 서버에 저장
    - 라이브러리 버전 자동 설정
    - 메트릭, 상태확인, 외부 구성 같은 프로덕션 준비 기능 제공
    - 세팅하는 시간이 감소
  • SOLID - 클린 코드로 좋은 객채 지향 설계의 5가지 원칙
    1. SRP : 단일 책임 원칙
    - 한 클래스는 하나의 책임만 가진다.
    - ex) UI 변경, 객체의 생성과 사용을 분리
    2. OCP : 개방 폐쇄 원칙
    - 확장에는 열려 있으나 변경에는 닫혀야 한다. 
    3. LIP : 리스코프 치환 원칙
    - 한 개의 기능에 정확성을 깨뜨리지 않아야 한다.
    4. ISP : 인터페이스 분리 원칙
    - 여러 개의 인터페이스를 분리해도 클라이언트에 영향을 주지 않아야 한다.
    5. DIP : 의존관계 역전 원칙
    - 프로그래머는 인터페이스에 의존해야지, 구현에 의존하면 안 된다.

2일차 - 예제 만들기

  • 테스트 코드를 작성할 경우 성공만 작성하는 게 아니라 실패 테스트 코드도 작성해야 한다. - 프로젝트 코드
  • 인터페이스(역할)와 클래스(구현) 반드시 나누어야 할 것 -> DIP
  • 한 개의 인터페이스 안에 여러 개의 구현체로 작성(다형성) -> 할인 예시(1000원, 10%)

3일차 - 스프링 핵심 원리

  • IoC, DI, 그리고 컨테이너
  • ComponentScan 방식과 의존관계 자동 주입
  • 생성자 주입, 필드 주입, setter 주입 장단점
    - 생성자 주입은 한 번밖에 주입되지 않아서 불변성과 final 키워드를 통해서 설정되지 않는 오류 X
    - 필드 주입은 코드가 간결해지는 장점은 있지만 외부에서 변경이 불가능하여 테스트하기가 힘들다.
    - 세터 주입은 의존관계를 선택/변경이 필요할 때 사용하지만 의존 관계는 안 바꾸는 게 좋다.
  • 내가 작성한 코드가 내가 제어권이 갖는 게 아닌 실행하고 제어하는 곳이 springboot, JUnit 대신 실행해주면
    프레임워크이다.
  • 라이브러리 json, xml 같은 라이브러리를 내가 직접 호출하는 경우는 라이브러리이다.

4일차 - 스프링 컨테이너와 스프링 빈

  • BeanFactory와 ApplicationContext
    - BeanFactory는 부모이며, 스프링 빈을 관리하고 조회하는 역할을 담당한다.
    - ApplicationContext은 BeanFactory 상속받아서 제공, 다른 인터페이스 부가기능(환경, 메시지, 리소스) 제공한다.
  • 다형성을 위해 구현 클래스가 여러 개 일 경우 @primary 또는 @Qualifier 사용
  • 빈 등록 초기화 및 소멸 메서드를 지정할 경우 @PostConstruct, @PreDestory 사용(라이브러리 적용 X)

5일차 - 싱글톤 컨테이너

  • 기존 자바에서 의존 관계를 설정할 경우 new 연산자를 통한 많은 인스턴스가 생기는 문제가 발생한다.
  • 또한 테스트가 어렵고 클라이언트가 구체 클래스에 의존하여 DIP 위반, OCP 위반한다.
  • 이러한 문제점을 스프링 컨테이너에서 해결한다. 
  • 스프링 컨테이너는 객체를 생성하고 공유하여 사용할 수 있도록 해준다. 덕분에 클라이언트가 요청이 올 때마다
    객체를 생성하는 것이 아니라 객체를 공유해서 효율적으로 사용한다.

6일차 - 빈 스코프

  • 스프링 빈의 이벤트 라이플 사이클
  • 외부에서 DI 받는 게 아니라 직접 필요한 의존관계를 찾는 것을 DL(Dependency Lookup) 의존관계
    조회라고 한다.
  • 프로토타입 스코프와 싱글톤 빈과 함께 사용 시 문제점 및 Provider 통한 문제 해결

7일차 - 로그 추적기

  • 어떤 부분에서 병목이 발생하는지, 그리고 어떤 부분에서 예외가 발생하는지를 로그를 통해 확인을 한다.
  • 로그 추적기를 어떻게 실행해야 하는지, 그리고 어떻게 동작하는지 설명해준다.
  • try_catch를 통해 시작, 끝, 예외 로그를 남긴다.
    - 하지만 코드가 길어지고 레벨 로그를 남기기 위해 클래스마다 파라미터로 넣어주고 있다.
    - catch에서 예외를 먹어서 정상 흐름으로 실행되므로 throw e 꼭 넣어줘야 한다. 

7일차 - 필드 동기화 (동시성 문제)

  • - 파라미터로 넘기지 않고 동기화할 수 있는 구현체를 만들어서 실행하기
    - 하지만 동시성 문제 발생 - 동시에 여러 사용자가 요청하면 여러 쓰레드가 동시에 애플리케이션 로직을 호출하게 돼서 로그가 섞여서 출력된다.
    - 싱글톤의 문제인 한 개의 인스턴스에 같은 필드를 여러 쓰레드가 동시에 접근해서 값을 중간에 변경해버린다.
    - 동시성 문제를 해결하기 위해 쓰레드 로컬을 사용한다.
    - 여러 쓰레드가 한 개의 필드값을 접근할 때 쓰레드 로컬을 통해 각각 전용 보관소를 만들면 각자 쓰레드가 필요한 데이터를 변환한다. 

  • 쓰레드 로컬을 사용할 때 주의할 점은 끝나는 시점에 남아있는 데이터를 'ThreadLocal.remove( )'를 통해서 꼭 제거해야 한다. 왜냐하면 WAS가 Thread Pool 안에 있는 쓰레드를 조회할 때 사용자 B는 사용자 A의 남아있는 데이터를 조회하는 문제가 발생한다.

8일차 - 템플릿 메서드 패턴

  • 각 클래스마다 try_catch 중복 코드가 생기는 문제점을 해결하기 위해서 템플릿 메서드 패턴을 적용
  • 변하는 부분과 변하지 않는 부분 == 비즈니스 로직과 부가 기능(try_catch)
  • 로그를 출력하는 템플릿 역할은 하는 변하지 않는 코드는 'AbstractTemplate'에 담아두고, 변하는 코드는 자식 클래스를 만들어서 분리한다. 

  • 단일 책임 원칙(SRP)을 보면 로그를 남기는 부분에 SRP을 지킨 것이다. 변경 지점을 하나로 모아서 변경에 쉽게 대처할 수 있는 구조를 만든 것이다.
  • 템플릿 메서드 패턴은 상속을 허용하기 때문에 자식 클래스 입장에서 부모 클래스의 기능을 전혀 사용하지 않는데, 상속을 하고 있다. 또한 잘못된 의존관계 때문에 부모 클래스를 수정하면 자식 클래스에게 피해를 줄 수 있다.

9일차 - 전략 패턴

  • - 전략 패턴은 상속이 아닌 위임을 통해 문제를 해결하는 방식으로, 인터페이스를 만들고 해당 인터페이스를 구현하도록 해서 문제를 해결한다.
  • 스프링에서 의존관계 주입에서 사용하는 방식이 바로 전략 패턴이다.
  • 전략 패턴의 핵심은 Context는 인터페이스에만 의존한다는 점이다. 덕분에 인터페이스의 구현체를 변경하거나 새로 만들어도 Context 코드에는 영향을 주지 않는다.

10일차 - 프록시, 프록시 패턴, 데코레이터 패턴

  • 프록시
    - Client -> Proxy -> Server
    - 주요 기능으로는 접근 제어(캐싱, 접근제한, 지연로딩), 부가 기능 추가(로그,변형) 2가지로 구분한다.
    - 클래스 의존관계를 보면 클라이언트는 서버 인터페이스( ServerInterface )에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다. 따라서 DI를 사용해서 대체 가능하다.
  • 프록시 패턴
    - 데이터가 한번 조회하면 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상 좋다. 이런 것을 캐시라고 하는데 프록시 객체를 통해서 캐시를 적용할 수 있다.
    - Client -> Interface -> Proxy 구현체, Data 구현체
    - Data 구현체에서 데이터를 조회하고 똑같은 데이터이면 proxy 구현체가 실행하는 구조이다.
    - 의도는 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공
  • 데코레이터 패턴
    - 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
    - Client -> Interface -> Data 구현체, Log 구현제, Time 구현체
    - 여기서 부가 기능은 기존에 하는 로직에서 로그를 추가를 하거나 필요한 다른 데이터를 추가하는 것이다.
    - 의도는 객체에 추가 기능을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공

11일차 - 인터페이스 기반 프록시 적용

  • 인터페이스 기반 프록시와 클래스 기반 프록시
  • 인터페이스가 없어도 상속을 통해 클래스 기반으로 프록시를 생성할 수 있다.
  • 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇 가지 제약이 있다.
    - 부모 클래스의 생성자를 호출해야 한다.
    - 클래스에 final 키워드가 붙으면 상속이 불가능하다.
    - 메서드에 final 키워드가 붙으면 해당 메서드를 오버 라이딩할 수 없다.

12일차 - 프록시 팩토리, 포인트컷, 어드바이스, 어드바이저

  • 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory)라는 기능을 제공한다.
    - 이전에는 상황에 따라서 JDK 동적 프록시를 사용하거나 CGLIB를 사용해야 했다면, 이제는 이 프록시팩토리 하나로 편리하게 동적 프록시를 생성할 수 있다.
  • Advice는 프록시에 적용하는 부가 기능 로직으로 InvocationHandler, MethodInterceptor 둘의 개념적으로 추상화한 것이다.
    - InvocationHandler == JDK 동적 프록시(인터페이스)
    - MethodInterceptor == CGLIB(클래스)

  • point : 어떤 메서드, 클래스 호출할건지
  • advice : 부가기능(로그)
  • advisor : 포인트컷+어드바이스
  • AOP 적용수만큼 프록시가 생성이 아닌 한 개의 프록시 안에 여러 어드바이저가 적용된다.
    ex) 한 개의 프록시 - loggin advisor, transaction advisor, security advisor
  • target 생성 -> ProxyFactory 생성 -> advisor(DefaultPointcutAdvisor(pointcut, advice))->getProxy 생성
  • 현재 문제점 configuration 안에서 빈으로 설정하는 것과 컴포넌트 스캔하는 경우 프록시 적용이 불가능하다.

13일차 - 빈 후처리기

  • BeanPostProcessor 인터페이스는 빈 저장소에 등록하기 직전에 조작할 때 빈 후처리기로 사용된다.
    - postProcessBeforeInitialization : 객체 생성 이후에 초기화가 발생하기 전에 호출되는 포스트 프로세서
    - postProcessAfterInitialization : 객체 생성 이후에 초기화가 발생한 다음에 호출되는 포스트 프로세서
    - 현재 컴포넌트 스캔으로 등록된 빈들을 조작하기 위해서 postProcessAfterInitialization 코드를 주입한다.

14일차 - AOP 빈 후처리기

  • 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 빈 후처리기가 스프링 빈에 자동으로 등록된다.
  • 전에는 BeanPostProcessor를 통해 빈 후처리기를 해줬지만 지금은 자동으로 프록시 적용대상체크
  • 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 자동으로 프록시 적용
  • 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이
    아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.

15일차 - AOP 핵심 기능과 부가 기능

  • 애스펙트
    - 부가 기능과 핵심 기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만드는 것
    - 포인트컷, 어드바이스, 어드바이저를 생성하고 보관하는 것을 담당(Aspect Advisor builder)
  • 조인 포인트
    - AOP를 적용할 수 있는 모든 지점
    - 스프링 AOP 경우 프록시 방식을 사용하므로 항상 메서드 실행 지점
  • AOP 프록시
    - AOP 기능을 구현하기 위해 만든 프록시 객체(JDK 동적 프록시, CGLIB 프록시)

16일차 - AOP 주의사항

  • 프록시와 내부 호출
    - 대안 1. 자기 자신 주입(세터주입)
    - 대안 2. 지연 조회(context)
    - 대안 3. 구조 변경(새로운 클래스 생성)
  • 타입 캐스팅
    - JDK 동적 프록시는 대상 객체인 구현체를 의존관계를 주입할 수 없다.
    - CGLIB 프록시는 대상 객체인 구현체를 의존관계를 주입할 수 있다.

17일차 - 엔티티 설계시 주의점

  • 스프링 부트와 JPA 활용 - 프로젝트 코드
  • 가급적 Setter 사용하지 않는 게 좋다. 유지보수가 어렵다.
  • 모든 연관관계는 지연 로딩으로 설정(N+1 문제)
    - @ManyToOne(fetch = LAZY), @OneToOne(fetch = LAZY)
  • 컬렉션은 필드에서 바로 초기화 하자. 
    - private List<Member> member = new ArrayList<>( );
  • 상위 엔터티에서 하위 엔터티로 모든 작업을 전파 
    - cascade = CascadeType.ALL
  • ​1:N 양방향
    ​- 연관관계의 주인일 때 @ManyToOne(fetch = LAZY), @JoinColumn(name = "id")
    - 아닌 경우 @OneToMany(mappedBy = "member")
  • 기본 생성자가 아닌 생성 메서드를 사용할 경우 제약을 둬야 한다.
    - @NoArgsConstructor(access = AccessLevel.PROTECTED)​​

'∙Java & Spring' 카테고리의 다른 글

ThreadPoolTaskExecutor 이용하여 성능 개선하기  (2) 2022.05.29
Junit Mock 사용하기  (0) 2022.05.06
JWT 구조  (0) 2022.04.16
자바의 정석 정리(2)  (0) 2022.03.09
자바의 정석 정리(1)  (0) 2022.03.02