ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 토비의 스프링 6 - 이해와 원리
    프로그래밍 지식/spring MVC study 2024. 10. 9. 21:12

    스프링 개발 시작하기

    스프링 프로젝트더라도 스프링 부트를 통해 셋업하면 용이

    : 스프링을 사용하게 되면 스프링 버전과 각각의 의존성 버전이 호환 가능한지 확인해야 함

    스프링 버전과 스프링부트의 버전은 다르니 유의

    PaymentService 개발

    프로젝트 생성 시 유의점

    • 자바 버전 확인
    • Gradle JVM 확인

    돈과 관련된 data type은 부동수소점(double, float)쓰면 안된다. BigDecimal을 쓰는 것이 보편적

    객체에 데이터가 들어가는 시점이 생성된 시점보다 한참 멀거나 여러 시점이 아니라면 (특히 생성되자마자 세팅된다면) setter보다 생성자를 만드는 것이 좋다. (IntelliJ에서 command + N)

    잭슨을 사용하여 json 데이터 처리 가능 (build.gradle에 spring-boot-starter-json 명시 필요)

    오브젝트와 의존관계

    오브젝트

    • 클래스의 인스턴스를 뜻함
    • 자바에서는 배열(Array)도 오브젝트

    의존 관계

    • A ---> B (A가 B에 의존하고 있다.)
    • 1) 클래스 사이의 의존관계 (코드 레벨)
      • A 클래스 코드는 B 클래스 코드가 존재해야만 제대로 동작할 수 있음을 의미
      • A가 B를 사용, 호출, 생성, 인스턴스화, 전송 (ex. 로컬 변수로 쓴다거나, 인스턴스 변수, 파라미터, return type으로 사용..)
      • B가 변경되면 A코드가 영향을 받는다.
    • 2) 오브젝트 사이의 의존관계 (런타임 레벨)

    관심사의 분리

    변경의 관점에서 보자

    자주 변경될 코드인지, 거의 변경되지 않는 코드인지

    ex. 기술적으로 변경이 생길 수 있는 코드(api 받아오는 등의) / 사양 or 로직에 따라 변경이 될 수 있는 코드

    다른 변경 포인트를 가진 코드를 한 함수에 두지 말자S

    함수 분리를 위해 추출이 필요할 수 있는데, 인텔리제이에서 추출을 지원함

    관심사에 따라 함수의 분리, 넘어서 클래스의 분리 필요

    상속을 통한 확장 

    IT업계에서 서비스를 판매할 때 주로 코드가 아닌 컴파일 된 클래스를 넘김

    원하는 부분만 바꿔서(Override하여) 사용할 수 있게끔 상속 가능하게 만들어서 넘김

     

    상위 클래스의 함수/클래스에 abstract 명시

    하위 클래스에 extends 명시 및 구현

     

    관심사가 다른 getExRate() 분리 후 상속하여 사용

     

    하지만 상속을 통한 확장은 많은 한계 존재

    • 강한 결합
      예를 들어, 부모 클래스의 메소드나 속성을 변경하면 이를 상속받는 모든 자식 클래스에 영향을 미치게 됨.
      이렇게 되면 클래스 계층이 커질수록 수정이나 유지보수가 어려워짐
    • 자바는 단일 상속밖에 되지 않기에 여러 부모 클래스의 기능이 불가. 코드 재사용의 유연성 떨어짐
    • 확장할 때 마다 앞에 이름을 붙이면 복잡해짐
      -> 대안 : 클래스의 분리

    클래스의 분리 (컴포지션)

    : 상속 대신, 다른 클래스의 기능을 객체로 포함하여 사용하는 방식

    • 이름이 간단해짐(상속인 경우에 뒤에 동일한 이름을 붙이기에)
    • 사용 의존관계가 만들어 짐

    상속 / 클래스 분리

    클래스의 분리 특징

    • 상속의 강한 결헙이 사라짐
    • 적절한 시점에 원하는 인스턴스를 만들어서 사용
    • 문제점 : 다른 클래스를 사용해야할 때 마다 일일히 클래스/인스턴스/함수 코드를 바꿔줘야 함

    인터페이스 도입

    • 서로 다른 클래스에 동일한 함수를 쓰게끔 강제함
    • 다른 클래스로 교체 시 클래스간 함수명이 달라서 생기는 변경점을 줄임
    • Overide 필요, public으로 구현
    • 하지만 상속은 PaymentService 클래스 수정이 필요하지 않았던 반면, PaymentService 클래스 수정이 필요한 단점이 존재
      -> 결국 앞의 방법들에 모두 단점이 존재

    그러나 PaymentService에 WebApiExRateProvider를 기입하므로, 추가적인 의존성이 존재

    관계설정 책임의 분리

    오브젝트 의존관계

    • PaymentService는 ExRateProvider에만 의존함을 기대했지만, WebApi.. 등 클래스에 의존함 (생성자에 new WebApi.. 부분이 있기 떄문에)
    • 어떤 클래스의 오브젝트를 사용하게 될 것인가 = 관계설정 책임의 분리
    • 관계 설정의 책임을 다른 곳에 넘긴다 (PaymentService가 아닌 Client로. 앞단으로)
    • Client에서 책임을 결정하여, 인자로 넘김

    어떤 클래스를 사용할지에 대한 책임을 Client가 수행하도록 옮김

    PaymentService에는 ExRateProvider(인터페이스 파라미터)를 받아서 사용

    어떤 인자를 넘길지는 Client가 결정
    -> 앞의 문제점 해소 가능.
    -> PaymentService/ExRateProvider를 수정할 필요 없이 사용자가 Client와 Provider만 수정하여 원하는 방식으로 사용 가능 

    오브젝트 팩토리

    현재 문제점 : Client가 지금 2가지 관심사를 가지고 있음

    • PaymentService를 이용해서 업무를 진행하는 것 (api return 등)
    • PaymentService가 어떤 Provider와 의존 관계를 맺는지에 대한 책임을 가지고 있음

    ObjectFactory를 만들어서 Provider와 관계를 맺는지에 대한 책임을 부여 (관심사 분리)

    원칙과 패턴

    : 객체지향 설계 원칙, 디자인 패턴

     

    객체지향 설계 원칙

    • 개방 폐쇄 원칙 (OCP)
      • 개방되어 있으면서 동시에 폐쇄
      • 확장에 열리고 변경에 닫힘
      • = 확장할 때 코드는 변경되면 안된다
    •  높은 응집도와 낮은 결합도
      • 응집도 높다 = 하나의 모듈이 하나의 책임 또는 관심사에 집중(단일 책임 원칙), 변화가 일어날 때 비용이 적게 든다 (A클래스 바꿨는데 B,C,D... 에 영향을 주면 안됨)
      • 결합도 낮다 = 느슨한 연결을 가져야 함 (A 수정하는데 B,C,D... 수정해야 되는게 강제됨, 높은 유지보수성 가짐)(클래스간 의존성 최소화)
    • 제어의 역전
      • 제어권 이전을 통한 제어관계 역전 - 프레임워크의 기본 동작 원리
      • 원래는 PaymentService가 어떤 Provider를 쓸 지 제어하였는데, 그 제어권을 다른 쪽으로 이전 (Client나 오브젝트 팩토리로)

    객체지향 디자인 패턴

    • 전략 패턴
      • 자신의 맥락에서, 필요에 따라서 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부에 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서(주입해서) 사용할 수 있는 디자인 패턴
      • ex. sort라는 맥락에 구체적인 알고리즘을 인자로 넣어서, 정렬을 다른 방식으로 하는 것. 대표적인 전략 패턴

    → 스프링이 이런 것들을 하게 도와준다.

    스프링 컨테이너와 의존관계 주입

    이제 스프링의 편리한 기능을 사용해보자!

    ObjectFactory를 BeanFactory로 대체

    ObjectFactory를 스프링에서 제공하는 BeanFactory로 대체

    BeanFactory를 구현하는 ObjectFactory에 구성정보 명시 (의존관계 명시)

    PaymentService(Bean)이 WebApiExRateProvider(Bean)에 의존하라고 BeanFactory가 제어를 함

    새로운 오브젝트 의존관계

    Client.java

    ...
    
    // ObjectFactory라는 구성 정보를 토대로 BeanFactory 생성해
    BeanFactory beanFactory = new AnnotationConfigApplicationContext(ObjectFactory.class);
    
    // 가지고 있는 빈중에 PaymentSerivce 타입의 빈을 가져와
    PaymentService paymentService = beanFactory.getBean(PaymentService.class);
    
    ...

     

    Configuration.java

    // 구성정보임을 명시하는 어노테이션 명시
    @Configuration
    public class ObjectFactory {
    	// 메소드에는 Bean임을 명시
    	@Bean
    	public PaymentService paymentService() {
            return new PaymentService(new WebApiExRateProvider());
        }
    }

     

    BeanFactory = 스프링 IoC/DI 컨테이너

    컨테이너 : 적재된 것들을 의미함. 오브젝트를 가지고 있다가 필요할 때 마다 줌. 

    • IoC(Inversion of Control) : 제어의 역전
    • DI : 의존성 주입

    구성정보를 가져오는 다른 방법

    ObjectFactory를 생성하지 않더라도 클래스에 직접 어노테이션을 명시하여 사용 가능

    • ObjectFactory 클래스에 @Bean 어노테이션 붙였던 메소드 삭제
    • PaymentService에 @Component 어노테이션 붙임
    • WebApiExRateProvider(사용할 구현체)에 @Component 어노테이션 붙임
    • ObjectFactory 클래스에 @ComponentScan 어노테이션 추가
      -> 너가 스캔해서 알아서 Bean간 의존성 추측해라
      -> 알아서 PaymentService -> WebApiExRateProvider 의존성을 만듦

    데코레이터 디자인 패턴

    오브젝트에 부가적인 기능/책임을 동적으로 부여

    -> 기존 코드는 수정하지 않고 기능을 추가하고 싶을 때 사용

    매번 Web.. 통해서 API 호출하지 않고, 일정 기간 동안은 캐시를 사용하고 싶은 경우

    Cached... 에서 필요 시에만 Web..을 호출하도록 한다.

    구성 정보만 수정하고, 다른 소스는 수정하지 않아도 됨

     

    의존성 역전 원칙

    1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. (둘 모두 추상화에 의존해야 한다.)

    2. 추상화는 구체적인 사항에 의존해서는 안 된다. (구체적인 사항은 추상화에 의존해야 한다.)

    잘못된 케이스

    당연히 PaymentService가 하위 모듈인 Web..을 호출하겠지만, 직접적인 의존 관계를 가지면 안된다.

    Web..이 바뀔 때 마다 PaymentService도 수정해야 할 여지가 있기 때문

    우리의 구조

     

    하지만 여전히 상위 수준의 모듈이 하위 수준의 모듈에 의존하고 있음.

    -> 인터페이스 소유권의 역전이 필요함

     

    인터페이스는 자신을 구현하는 모듈(패키지)에 있기 보다는, 사용하는 모듈(패키지에) 있게끔 하자

    테스트

    자동으로 수행되는 테스트

    스프링이 테스트 작성에 많은 도움을 준다.
    지금까지 계속 main 함수를 호출하여 출력을 확인하는 테스트를 함(수동 테스트)
    수동 테스트의 한계

    • 프린트된 메시지를 수동으로 확인하는 방법은 불편
    • 사용자 눈에 보이기까지 개발한 뒤에 확인하는 방법은 테스트가 실패했을 때 확인할 코드가 많다.
    • 테스트할 대상이 많아질수록 검증하는데 시간이 많이 걸리고 부정확함

    (작은 크기의) 자동 수행되는 테스트, 개발자가 만드는 테스트

    • 개발한 코드에 대한 검증 기능을 코드로 작성
    • 자동으로 테스트 수행하고 결과 확인
    • 테스팅 프레임워크 활용 (편리함)
    • 테스트 작성과 실행도 개발 과정의 일부 

    JUnit 테스트 작성

    JUnit 5

    • @Test를 붙이면 테스트 메소드로 인식하고 테스트 수행
    • @BeforeEach : 각 테스트 전에 실행됨.
      예를 들어 클래스에 5개의 테스트가 존재하면, BeforeEach - 테스트1 - BeforeEach - 테스트2 ..
    • 테스트마다 새로운 (클래스의) 인스턴스가 만들어짊

    기존 소스 : src/main/java/패키지
    테스트 소스 : src/test/java
    이 뒤에 동일한 패키지 만들고, 동명의 클래스 뒤에 Test를 붙이는 것을 권장
    @Test가 org.junit.jupiter.api인지 확인

    package tobyspring.hellospring;
    
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    
    import java.util.Arrays;
    import java.util.List;
    
    public class SortTest {
    	// 준비
        Sort sort;
    
        @BeforeEach
        void beforeEach() {
            sort = new Sort();
            System.out.println(this);
        }
    
        @Test
        void sort() {
        	// 실행
            List<String> list = sort.sortByLength(Arrays.asList("aa", "b"));
    	// 검증
            Assertions.assertThat(list).isEqualTo(List.of("b", "aa"));
        }
    
        @Test
        void sort3Items() {
            List<String> list = sort.sortByLength(Arrays.asList("aa", "ccc", "b"));
    
            Assertions.assertThat(list).isEqualTo(List.of("b", "aa", "ccc"));
        }
    
        @Test
        void sortAlreadySorted() {
            List<String> list = sort.sortByLength(Arrays.asList("b", "aa", "ccc"));
    
            Assertions.assertThat(list).isEqualTo(List.of("b", "aa", "ccc"));
        }
    }


    테스트에는 크게 3가지 작업이 존재

    • 준비 (db 연결 등)
      • sort 클래스를 확인하려면 인스턴스가 필요
    • 실행
      • 정렬이 안된 리스트를 정렬함
    • 검증 (실행 결과를 확인)
      • Assertions
      • org.assertj.core.api 를 권장

     
    모든 테스트 코드 내에서 중복이 존재한다면?
    -> @BeforeEach를 사용하여, 각 테스트가 실행되기 이전에 실행되게끔 한다.


    테스트는 작성도, 실행도 빨라야 유효하다.
    테스트 클래스도 실행되기 위해서는, 인스턴스로 만들어야하는데 이것은 JUnit이 만들어 준다.
    매 테스트가 진행될 때 마다 새로운 인스턴스를 만들어준다. (각 테스트의 독립성을 보장)

    PaymentService 테스트

    PaymentService 요구사항에 명세된 기능에 대해 검증 필요
    인텔리제이에서 테스트 클래스를 만들어준다.

    -> 테스트 대상 클래스에서 - Generate - Test : 테스트 템플릿 생성


    메소드별로 검증하고자 하는 내용을 주석처럼 써주면 좋다.
    @DisplayName("한글로 구체적으로 적을 수 있음") -> 테스트 실행 시, 콘솔에 출력이 되므로 파악이 용이함

     

    타겟 메소드에 예외를 던질 수 있는 로직이 있는데, try/catch나 예외 처리를 하지 않아서 빨간 줄이 뜸 -> 어떻게 해결할까

     

    테스트의 실패 2가지 

    • 기대값과 달라서
    • 예외가 던져져서 끝까지 수행 불가

    결론 : 예외가 발생하면 예외를 던지도록 해라. 예외를 던지는 것도 Fail 케이스 이므로

    @Test
    void prepare() throws IOException { // throws IOException 붙여서 예외를 던지도록 함
    
    }

     

     

    WebAPI를 통해서 환율을 가져오면.. web에서 제공해주는 환율이 계속 바뀌기 때문에 어떻게 테스트 가능할지?
    -> 가져온 환율값이 Null이 아님으로 간이 테스트
    유효시간이 30분인지를 어떻게 알 수 있을지?
    -> 유효시간이 현재 시간보다는 후에 있고, 현재  시간에서 30분을 더한 시간보다는 앞에 있다고 간이 테스트

    테스트의 구성 요소

    방금 했던 테스트의 문제점

    1. 우리가 제어할 수 없는 외부 시스템(외부api)에 문제가 생기면?
    2. 정말로 ExRateProvider가 제공된 환율 값으로 계산했나?
    3. 환율 유효 시간 계산은 정확한가?

    테스트의 구성 요소

    • 테스트 대상 (SUT, System Under Test) (ex.sort)
    • 테스트 (독립적인 object) (ex.sortTest)
    • 협력자 (WebApiExRateProvider, 선택적)
      • ex. WebApiExRateProvider. PaymentSerice가 호출하는 API다. 그런데 우리가 WebApiExRateProvider(협력자) 도 테스트하고 싶은가?
      • 우리는 협력자들을 테스트하고 싶은 것은 아님. 그것들은 다른 테스트의 테스트 대상이 됨.
      • 그러므로 Stub(테스트용 오브젝트)를 만듦. ExRateProviderStub이라는, 테스트가 돌아가는 동안에만 우리가 제어할 수 있는 Stub(대체 오브젝트)을 사용하여 독립적인 테스트를 만들음.

    WebApiExRateProvider가 아닌 ExRateProvider를 호출하게끔 함 (이게 가능한 이유도 우리가 앞에서 설계를 했기 때문)

    Stub은 테스트 대역(대역배우의 대역)이라고도 부름

     

    테스트와 DI
    Stub은 테스트 패키지에 만듦 (테스트에만 필요하기 때문에) = mock과 비슷
    인터페이스를 구현한 Stub을 만듦 (생성자로 넣은 값을 그대로 뱉는)
    -> API(협력자)를 테스트 대상에서 제외하고 의존성을 없앰
    외부에서 의존성을 주입하게(DI) 설계 하지 않았다면(Web..아닌 Stub을 바라보도록), 테스트를 만들기 쉽지 않았을 것. DI 중요하다.

    : 현재까지 진행했던 방식은 수동 DI를 이용하는 테스트

     

    스프링 DI를 이용하는 테스트

    -> 코드가 아닌 구성 정보를 이용하여 컨테이너로부터 테스트 대상을 가져와서 테스트 (@ContextConfiguration, @Autowired)

     

    스프링 컨테이너 테스트

    방법 1

    테스트용 구성정보도 별도로 만들어 줌 (TestObjectFactory)
    그 안에서 Web.. 이 아닌 Stub을 사용하도록 의존 관계 설정

    class PaymentServiceSpringTest {
    
    	@Test
        void convertedAmount() throws IOException {
        	BeanFactory beanFactory = new AnnotationConfigApplicationContext(TestObjectFactory.class);
            PaymentService paymentService = beanFactory.getBean(PaymentService.class);
            
            Payment payment = paymentService.prepare(1L, "USD", BigDecimal.TEN);
            
            assertThat ...
            
        }
    }

    방법 2

    @ExtendWith -> Junit에서 스프링을 사용하게끔 함

    @ContextConfiguration : 구성 정보 등록

    @Autowired -> 와이어링 : 의존관계를 주입하는 것. 자동으로 의존 관계를 주입하라는 의미. 구성 정보에 명시된, Type이 일치하는 스프링 빈을 가져옴

    -> 수동 DI가 더 빠르긴 하다.


    학습테스트
    직접 만들지 않은 코드, 라이브러리, 레거시 시스템에 대한 테스트
    (이용하지만 통제할 수 없는, 확신을 가질 수 없는 것들에 대한 테스트)


    -> 테스트 대상의 사용방법을 익히고 동작방식을 확인하는데 유용하다 (검증)
    -> 버전업이 되었을 때, 이전과 동일하게 동작하는지를 검증할 수 있다.


    시간을 테스트하기 위해 자바에 내장된 'Clock'을 사용할 예정인데, 이것에 대한 학습테스트가 필요

    Clock을 이용해서 fixed 시간을 만든 다음에, 일정 시간(예를 들면 정확히 30분)까지는 원하는 동작을 하는지 확인

    (인텔리제이 라이브 템플릿 이용해서 테스트 코드 작성 시간을 줄일 수 있다. 단축키 asj-> Assertions.assertThat(커서)까지 만들어 줌)

     

     

     

     

    템플릿

    스프링 버전 업그레이드 하는 방법

    1. IntelliJ에서 'External libraries'에서 내 스프링 버전 확인
    2. Spring Initailizer를 통해 원하는 스프링 부트 버전(부트 버전을 설정하면 스프링 버전도 따라옴)의 프로젝트를 임의로 만듦
      : 호환되는 Java version 유의
    3. 생성된 새로운 프로젝트와 기존 프로젝트간 세 파일을 비교
      1. gradle/wrapper/gradle-wrapper.properties
        : 그대로 붙여넣기
      2. build.gradle
      3. setting.gradle
    4. Gradle Load (코끼리 누르고 새로고침)
    5. External libraries에서 스프링부트 버전 변경되었는지 확인

    템플릿

    코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법

    • 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분 자유롭게 변경되는 성질을 가진 부분 : 템플릿
    • 자유롭게 변경되는 성질을 가진 부분 : 콜백

    (강의 외 내용 : 체크드 예외/언체크드 예외)
    체크드 예외 : 안정성 보장

    • 특정 작업에서 예상되는 오류를 명시적으로 다루도록 컴파일러가 강제
    • 예를 들어, 파일 작업(IOException)에서 반드시 예외를 처리해야만 작업 실패로 인한 시스템 충돌을 예방 가능
    • 처리 방법 : try-catch, throws
    • 예시
      • 파일이 없을 때(FileNotFoundException)
      • 네트워크 연결이 끊어졌을 때(SocketException)
      • 데이터베이스가 응답하지 않을 때(SQLException)

    언체크드 예외 : 유연성, 간결성 제공

    • 예외를 꼭 처리하지 않아도 되는 상황에서는 언체크드 예외를 사용해 불필요한 코드 작성을 피할 수 있음
    • 호출자에게 "내가 책임질게!"라기보다는 "알아서 해봐"라는 신호를 보냄
    • 예시
      • RuntimeException

    변하는/변하지 않는 코드 분리

    변하는 부분 -> 메소드 추출

    콜백 : 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트
    파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키는 것이 목적
    -> 메소드 주입 (의존관계 주입의 한 종류)

    스프링이 제공하는 템플릿

    스프링이 기본적으로 제공하는 템플릿 존재

    예외

    예외 : 정상적인 프로그램 흐름을 방해하는 사건

    예외 발생 시
    - 예외 상황을 복구해서 정상적인 흐름으로 전환할 수 있는가?
    : 재시도, 대안
    - 버그인가?
    : 코드의 버그인지, 클라이언트의 버그인지
    - 제어할 수 없는 예외상황인가?

    예외를 잘못 다루는 코드

    - 예외를 무시하는 코드
    catch(SQLException e) {}
    - 무의미하고 무책임한 throws
    throws Exception {} (모든 예외를 그냥 상위로 던지고 던지고..)

    에러 : 시스템에 비정상적인 상황이 발생 (OutOfMemoryError, TreadDeath -> 캐치할 수 있지만 캐치해봐야 소용 없는.. 에러)

    예외의 추상화와 전환

    기술에 따라 같은 문제에 대해 다른 종류의 예외가 발생하는 경우가 존재.. bad
    적절한 예외 추상화와 예외 번역이 필요 (기술이 다르더라도 동일한 에러면 동일 예외코드)

Designed by Tistory.