싱글톤 방식의 주의점

 

무상태(stateless)로 설계해야 하는 이유

웹 어플리케이션 설계에서 유용한 싱글톤 방식을 사용할 때는 싱글톤 클래스를 반드시 무상태로 설계해야 한다. 다음 예제를 보자.

 

: StatefulService.java

: StatefulServiceTest.java

 

해당 코드에서는 ThreadA에서 userA가 주문을 하고, 가격을 확정하기 전에 ThreadB에서 userB가 다른 상품을 주문한다.

싱글톤 클래스라 클래스 안의 필드도 공유하고 있기 때문에, 이 경우 userA가 주문했던 금액과는 다른 금액이 나오게 된다.

무상태로 설계하기 위해서는 기존의 클래스 안의 멤버 변수(필드)를 지역 변수로 바꾸거나, ThreadLocal을 사용해야 한다.

 

지역 변수를 사용한 예시

StatelessService.java

더보기
package hello.core.singleton;

public class StatelessService {

    public int order(String name, int price){
        System.out.println("name = " + name + ", price = " + price);
        return price;
    }

}

StatelessServiceTest.java

더보기
public class StatelessServiceTest {

    @Test
    @DisplayName("싱글톤 클래스를 무상태로 설계해야 한다.")
    void statelessServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatelessService statelessService1 = ac.getBean("statelessService", StatelessService.class);
        StatelessService statelessService2 = ac.getBean("statelessService", StatelessService.class);

        int priceA = statelessService1.order("userA", 10000);
        int priceB = statelessService2.order("userB", 20000);

        assertThat(priceA).isEqualTo(10000);
        assertThat(priceB).isEqualTo(20000);

    }

    @Configuration
    static class TestConfig {

        @Bean
        public StatelessService statelessService() {
            return new StatelessService();
        }
    }

}

 

ThreadLocal이란?

스레드(Thread)의 개념

프로그램을 실행할 때, 스레드(thread) 프로세스(process)라는 말을 사용한다.

 

 프로세스: 운영체제로부터 자원을 할당받는 작업의 단위.

여러 개의 어플리케이션을 사용한다면, 여러 개의 프로세스(multi-process)가 동작 중이다.

 

 스레드: 프로그램(프로세스) 실행의 단위.

하나의 어플리케이션(프로세스)를 실행할 때도 여러 개의 스레드(multi-thread)를 사용할 수 있다.

 

 멀티 프로세스 -> 멀티 스레드

 멀티 스레드 -> 멀티 프로세스

 

변수의 생성 영역(메모리)

객체나 변수를 생성할 때 Heap영역, 또는 Stack영역에 위치시킬 수 있다.

 

 Heap: 모든 스레드에서 공유하는 영역.

ex) 정적(static) 변수

 Stack: 하나의 스레드에서 사용하는 영역.

ex) 지역(local) 변수

 

ThreadLocal의 구조

ThreadLocal스레드 정보를 key로 사용하여 값을 저장하는 Map 구조를 갖고 있다.

멀티 스레드 환경에서 Stateful 클래스를 싱글톤으로 선언하고 싶을 때, ThreadLocal을 사용하면 각 스레드 별로 다른 변수를 사용할 수 있다.

 

ThreadLocalService.java

더보기
package hello.core.threadlocal;

public class ThreadLocalService {

    private static ThreadLocal<String> product = new ThreadLocal<>();
    private static ThreadLocal<Integer> price = new ThreadLocal<>();

    public ThreadLocalService() {}

    public void setProduct(String item) {
        product.set(item); // set()으로 value 값 지정 가능
    }

    public void setPrice(Integer value) {
        price.set(value);
    }

    public void printOrder() {
        System.out.println("product = " + product.get() + ", price = " + price.get());
				// get()으로 value 값 반환 가능
    }
}

ThreadLocalServiceTest.java

더보기
package hello.core.threadlocal;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


public class ThreadLocalServiceTest {

    @Test
    void threadLocalOrder() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        ThreadLocalService tls = ac.getBean("threadLocalService", ThreadLocalService.class);

        Integer priceA = 10000;
        Integer priceB = 20000;
        String productA = "strawberry";
        String productB = "watermelon";

        Thread threadA = new Thread(()->{
            tls.setPrice(priceA);
            tls.setProduct(productA);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tls.printOrder();

        });

        Thread threadB = new Thread(()->{
            tls.setPrice(priceB);
            tls.setProduct(productB);
            tls.printOrder();
        });

        threadA.start();
        threadB.start();

    }

    @Configuration
    static class TestConfig {

        @Bean
        ThreadLocalService threadLocalService() {
            return new ThreadLocalService();
        }
    }

}

 

ThreadLocal 사용할 때 주의점

⚠️ 메모리 문제

ThreadLocal은 사용 시 메모리를 관리해야 한다.

ThreadPool을 사용하면 기존에 생성했던 ThreadLocal을 재사용할 수 있는데, 이때 이전에 삭제되지 않은 메모리가 남아있을 수 있다.

ThreadPool을 사용할 경우 반드시 ThreadLocal.remove()를 이용하여 남은 데이터를 제거해 주어야 한다.

 

*참고한 포스트들*

 

@Configuration과 싱글톤

스프링 컨테이너는 @Configuration 으로 등록한 클래스에서 빈을 생성 및 조회하는데, 이 빈들을 모두 싱글톤 타입으로 만든다고 했다.

그런데 @Configuration으로 등록된 AppConfig 클래스의 각 빈 메소드에서는 new() 생성자로 객체를 생성한다.

어떻게 각 빈을 호출하면서 각 객체는 한 번만 생성하는 것이 가능할까?

 

: AppConfig.java

 

테스트를 위해서 각 구현 클래스(MemberServiceImpl.java, OrderServiceImpl.java)에 MemberRepository 객체를 조회할 수 있는 클래스를 임시로 작성하고, 결과를 보자.

 

: ConfigurationSingletonTest.java

 

모든 MemberRepository 객체의 주소값이 같다.

즉 MemberRepository 객체는 딱 한 번만 생성되고, 다른 클래스는 오직 하나의 객체를 공유하고 있다.

⇒ 싱글톤 패턴을 잘 준수한다.

 

AppConfig.java 파일을 변경하여, 각 빈이 호출될 때마다 로그를 남겨서 확인해 보자.

결과는 다음과 같다.

빈이 한 번씩만 호출되었다. 스프링 컨테이너는 인스턴스를 한 번 생성하고 나면 해당 빈 메소드를 더 이상 호출하지 않은 것이다.

 

@Configuration과 바이트코드 조작

빈이 한 번씩만 호출된 이유는 스프링 컨테이너가 바이트 코드를 조작해서, 사용자가 입력한 @Bean과는 다른 클래스를 만들고 그 클래스를 통해 싱글톤 패턴을 사용하고 있기 때문이다.

 

: ConfigurationTest.java

 

스프링 컨테이너는 싱글톤 패턴을 유지하기 위해서 사용자가 @Configuration에 등록한 코드를 조작해서 새로운 클래스를 만든다. 그 클래스가 스프링 빈으로 등록된 것이다.

 

바이트 코드 조작

바이트 코드란?

 바이트 코드

고급 언어(프로그래밍 언어. 자바 언어)로 작성된 코드를 가상머신(여기서는 JVM)이 이해할 수 있는 중간 단계의 코드로 컴파일한 언어.

기계어보다는 추상적(high-level)이고 고급 언어보다는 low-level이다.

 

코드 조작이 가능한 이유

🔑 바이트 코드를 조작하면 실행 결과가 달라지는 이유

자바 코드는 1차로 JVM이 처리할 수 있는 바이트 코드로 변환된다. 그리고 그 바이트 코드를 읽어서 비로소 프로그램이 실행된다.

바이트 코드 내용대로 실행이 되기 때문에, 중간 단계에서 코드가 조작된다면 결과값도 바뀌게 된다.

참고한 포스트: [Java] 바이트코드 조작 (tistory.com)

 

코드를 조작하는 방법(예시)

💡 byteBuddy 라이브러리

바이트 코드를 조작하는 코드를 작성하고, 결과를 다른 파일에 저장한다고 치자.

저장한 파일은 원본과는 다른 코드를 갖고 있다.

참고한 포스트: 자바 바이트 코드 조작하는 방법 (tistory.com)

 

바이트 코드 조작의 결과

아마도 바이트 코드를 조작해서, 이런 싱글톤 클래스를 만들었을 것이다.

  1. 사용자가 빈으로 등록한 클래스가 이미 스프링 컨테이너에 있다 → 해당 인스턴스 반환.
  2. 스프링 컨테이너에 사용자가 빈으로 등록한 클래스가 없다 → 새로운 인스턴스 생성.

 

*정리*

@Configuration은 싱글톤 패턴으로 빈을 생성하는 어노테이션이다. @Configuration이 없어도 빈은 생성된다! 다만 싱글톤 타입이 아니다.

 

@Configuration을 지우고 AppConfig.java 를 참고하여 코드를 실행한 결과는 다음과 같다:

 

* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-04 #

 

#목차#

21. 순수 JDBC

22. 스프링 통합 테스트


21. 순수 JDBC

 

이번 시간에서는 데이터를 다루는 스프링 라이브러리 중 하나인 Spring JPA를 살펴보기 전, 이전에는 데이터를 어떻게 처리했는지를 보았다. 지금은 사용하지 않는 방법이므로 가볍게 보려고 한다. 

 

이전에는 'JDBC'를 사용했으며, (1) build.gradle 파일에서 implementation 선언을 한 뒤 (2) application.properties 파일에서 세부 설정을 해야 했다. 

 

+Jdbc(Java DataBase Connectivity)란, 자바에서 데이터베이스에 접속할 수 있도록 해 주는 API이다. 

 

1. 사전 환경설정

(1) build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	runtimeOnly 'com.h2database:h2'
}

이렇게 jdbc와 사용할 데이터베이스 h2를 import하는 과정이 필요하다. 

 

(2) application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

해당 파일에서는 import를 한 뒤에 추가로 이뤄져야 하는 세부 설정을 관리한다. 예를 들면

[1] tcp(소켓이라고 한다. 여러 곳에서의 접속을 관리하는 용도라고 하는데, 후에 더 자세히 알아봐야겠다.)를 통해 어떤 경로로 접속할지를 명시하거나,

[2] Jdbc driver로 어떤 데이터베이스를 연결할지 해당 클래스를 명시하거나,

[3] H2 데이터베이스의 접근 권한을 받아내는 데 필요한 유저 이름, 비밀번호 등의 정보를 입력한다. 

이런 정보를 해당 파일에서 관리했다. 

 

2. DB 리포지토리 클래스 구현

앞서 DB를 사용하기 전에는 임시로 메모리를 사용하는 MemoryMemberRepository 클래스를 만들어서 DB를 대신하였다. 이제는 실제 DB에 연결했으므로 새로운 JdbcMemberRepository 클래스를 만들었다. 

(클래스 코드가 정말 복잡하고 길다. 지금은 이렇게 개발할 일이 없으므로 생략!)

 

리포지토리를 구현했다 치면, 이제는 리포지토리 역할을 해 오던 MemoryMemberRepository 클래스를 JdbcMemberRepository 클래스로 교체해 주면 된다. 

 

앞서 의존성 주입(DI)와 스프링 빈 등록 과정, DB 인터페이스를 사용하면 단 한 줄의 코드만 바꿔서 이 작업을 대신할 수 있다. 

@Bean
public MemberRepository memberRepository(){
    return new JdbcMemberRepository(dataSource);
    // 기존 코드에서는 MemoryMemberRepository()였다. 
}

또한 앞에서 궁금했던 '형 변환'의 이유도 여기서 밝혀졌다. 

형 변환이란 하위 클래스의 인스턴스를 생성할 때, 그 클래스의 타입을 자신의 상위 클래스나 인터페이스 타입으로 선언하는 것을 의미한다.

아래의 코드가 그 예시이다. 

MemberRepository memberRepository = new JdbcMemberRepository();

이처럼 해당 인스턴스를 상위 클래스나 인터페이스 타입으로 선언하면, 후에 구현체나 하위 클래스를 바꿀 때 해당 코드만 바꿔 주면 된다. (하위 클래스들의 메소드들은 모두 상위 클래스/인터페이스에게서 상속/구현받았다고 전제할 때 해당한다.) 

이를 '다형성'이라고 한다. 그때그때 필요에 맞춰서 해당 인스턴스가 다양한 타입으로 변화할 수 있어서 다형성이라고 말하는 것 같다. 

 

또한 위처럼 인터페이스와 다형성을 사용한 개발은 개발의 SOLID 원칙 중 하나인 개방-폐쇄 원칙(Open-close principle)을 지켰다고 볼 수 있다. 

폐쇄적으로 부분의 코드만 수정해도 코드가 프로그램 전반에 개방적으로 영향을 줄 수 있다는 의미인 듯 하다. 


22. 스프링 통합 테스트

통합 테스트와 단위 테스트

스프링 통합 테스트란, 테스트 시 스프링 환경을 사용하는 테스트이다. 스프링 통합 테스트에서는 스프링과 연결된 다른 프로그램들을 전체적으로 사용한다. 앞서 스프링과 DB를 연결하였으므로, 여기서는 DB와 연결된 테스트를 진행할 수 있다. 

통합 테스트의 반대말은 단위 테스트(unit test)이다. 강의에서는 단위 테스트가 대체로 더 좋은 테스트이고 에러를 잘 잡아낼 수 있는 테스트라고 언급했다. 

 

스프링 통합 테스트를 사용하기 위해서는 해당 테스트 클래스에다가 @SpringBootTest 어노테이션을 붙여 준다. 그러면 이 클래스 안의 테스트들은 스프링 환경에서 실행된다. 

 

실제로 통합 테스트를 실행할 때

또한, 현재 프로그램은 DB와도 연결되어 있기 때문에 DB에 있는 기존 데이터가 테스트의 결과에 영향을 줄 수 있다. 

이를 방지하기 위해서 실제 테스트를 할 때에는

(1) 테스트 전용 DB를 따로 만들어서 실행한다. 

(2) @Transactional 어노테이션을 사용하면 개별 테스트를 실행할 때마다 테스트에서의 변경 사항을 DB에 저장하지 않고 롤백(roll-back), 즉 쿼리가 실행되지 않은 상태로 되돌려 준다.

 

DB의 쿼리 처리

@Transactional은 DB가 쿼리를 처리하는 방식과 관련이 있다. 

DB는 SQL문 등의 쿼리를 받자마자 실행하는 것이 아니다. 쿼리를 받고, 커밋(commit)을 해야만 해당 내용이 실행되어 DB 내부에 변화가 일어난다. 

따라서 Transactional은 엄밀히 말하면 '쿼리 실행 전으로 상태를 되돌리는 것' 이 아니라, '쿼리를 쌓아두고(커밋하지 않고) 있다가 테스트가 끝나면 종료시키는 것'에 가깝다.

 

+뿐만 아니라, 테스트 환경에서도 필드나 생성자에 @Autowired 어노테이션을 설정할 수 있다.

 

* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-04 #

 

#목차#

18. 회원 웹 기능 - 등록

19. 회원 웹 기능 - 조회


18. 회원 웹 기능 - 등록

 

폼(form)을 사용해서 회원가입을 위한 이름을 입력값으로 받고, 이를 저장소에 반영하는 기능이다. 

여기서는 MemberForm.java 파일을 따로 만들어서 폼 기능을 구현하였다. 

+ django에서는 forms.py로 아예 폼들을 따로 모아서 models.py에 있는 모델과 분리시키던데, 스프링에서도 이런 방법이 자연스러운 것인지는 아직 잘 모르겠다. 

 

여느 폼 기능과 똑같이, 폼 클래스에서는 받아야 할 입력값들을 private한 필드로 선언한다. 그리고 getter, setter 메소드만 public으로 선언한다. 

 

여기서는 REST API 방식 대신 MVC 방식을 사용하여 각 페이지/기능에 맞는 html 페이지가 따로 존재했다. 

 

HTTP 요청 주고받는 순서

1. HTTP GET 방식으로 form을 제공하는 페이지에 접속한다.

2. 필요한 값을 입력하고 등록 버튼을 누른다. 

3. 해당 폼은 html의 form 태그 안에서 만들어진 폼이며, 이 경우 form 태그는 action=""으로 입력된 폼을 어떤 주소로 보낼지에 대한 정보를 담고 있다. 

등록 버튼을 누른 순간 해당 url 주소로, HTTP POST 방식으로 폼이 전송된다. 

4. html의 form 태그에 명시된 해당 주소로 가면 POST 방식으로 들어온 요청을 처리하는 메소드가 있다(있다고 가정한다).

5. 해당 메소드에서는 매개변수로 MemberForm 객체를 받고, 해당 객체의 .getName() 등의 메소드(MemberForm 내부에서 정의된 메소드)를 통해서 사용자가 입력한 값에 접근하고 추가적인 처리를 할 수 있다.


19. 회원 웹 기능 - 조회

 

조회 기능은 GET 방식으로 이뤄지며, 여기서는 저장소 역할을 하는 List 객체 안에 저장된 모든 member의 정보를 꺼내보는 기능이다. 

 

즉 데이터 저장소(여기서는 실제 DB가 아니지만 DB와 동일하다고 봐도 무방하다)에 접근해야 하는데, 그러려면 컨트롤러(controller) 클래스에서 리포지토리(repository)의 기능을 사용해야 한다. 

 

다행히 앞서 컴포넌트 스캔 또는 자바 방식으로 스프링 빈을 직접 등록함으로써 이 문제는 이미 해결되었다.

싱글톤 패턴을 사용하면서 컨트롤러는 서비스에, 서비스는 리포지토리에 의존하고 있기 때문이다. 

 

따라서 컨트롤러 클래스에서는 private 타입으로 선언한 서비스 클래스 객체를 호출한 다음, 해당 객체가 가진 join() 메소드를 호출하면 된다.

그러면 서비스 클래스에서는 private 타입으로 선언한 리포지토리 클래스 객체를 갖고 있기 때문에, 결과적으로 데이터를 저장하는 객체에 접근해서 데이터를 꺼내올 수 있다. 

 

다만 현재는 웹 MVC 방식을 사용하기 때문에 스프링에서는 직접 데이터를 List 형태로 꺼내지 않고 웹 템플릿에 넘긴다. Spring에서는 이럴 때 Model 객체를 사용한다. 

즉 Model 타입의 객체를 매개변수로 받아서, 해당 model 객체에 addAttribute()로 속성을 추가한다. 이후 템플릿 이름을 호출하면 해당 model 객체는 호출한 템플릿에서 변수로 사용될 수 있다. 

 

이로써 간단한 등록 및 조회 기능을 구현하였지만 한계가 있다. DB나 파일 등을 사용해야만 서버를 껐다가 켜도 해당 데이터가 남아 있다. 따라서 다음 시간에는 Spring에서 DB를 사용하고 연결하는 방법에 대해 포스팅하려고 한다. 

 

* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-04 #

 

#목차#

15. 컴포넌트 스캔과 자동 의존관계 설정

16. 자바 코드로 직접 스프링 빈 등록하기


15. 컴포넌트 스캔(Component scan)과 자동 의존관계 설정

 

컴포넌트 스캔이란?

컴포넌트(@Component)는 스프링에서 관리하는 클래스 앞에 붙는 annotation(@)이다. @Component가 붙은 클래스는 자동으로 스프링이 관리한다. 앞서 컨트롤러 클래스에는 @Controller라는 annotation을 붙였는데, 이것도 컴포넌트에 해당한다. (@Controller annotation 클래스를 보면 해당 클래스에도 @Component annotation이 붙어있기 때문이다.)

 

그런데 스프링이 클래스들을 관리하는 방법은 컴포넌트 스캔뿐만이 아니다. 자바 코드를 사용해서 직접 스프링 빈(@Bean)을 등록할 수도 있다. 강의에서는 각각의 장단점이 있고 섞어서 사용할 수도 있으니 어쨌든 두 방법을 모두 알긴 알아야 한다고 하셨다. 

 

우선 컴포넌트 스캔 방법부터 다뤄보자. 앞서 컨트롤러, 서비스, 리포지토리 클래스를 만들었다. 이 클래스들 역시 각자의 역할이 있고(컨트롤러-서비스-리포지토리 방식 구성), 따라서 컴포넌트 스캔을 할 수 있다. 정확히 말하면, 이 [컨트롤러-서비스-리포지토리] 구성이 정말 자주 사용되는 구성이다 보니 스프링에서는 각각의 구조를 위한 annotation이 따로 있는 것이다. 

 

그래서 서비스 클래스에는 @Service, 리포지토리 클래스에는 @Repository annotation을 붙여주면 스프링에서 이 클래스들을 관리하고 필요할 때 알아서 배치할 수 있다. 

 

컴포넌트 스캔이 필요한 이유

그런데 앞서 작성한 코드로도 충분히 컨트롤러-서비스-리포지토리의 구조를 갖춘 것 같은데 왜 컴포넌트 스캔이 필요할까? '자동 의존관계 설정(Dependency Injection. DI)' 때문이다. 

 

해당 구조에서는 컨트롤러는 서비스에게, 서비스는 리포지토리에게 '의존'한다. 즉 컨트롤러의 메소드는 서비스가 없으면 작동할 수 없고, 서비스의 메소드도 리포지토리가 없으면 실행될 수 없다. 이러한 관계를 의존관계(Dependency)라고 한다. 그래서 컨트롤러나 서비스의 생성자에 매개변수로 서비스나 리포지토리 타입의 매개변수를 넣는 식으로 이 의존관계를 해결했던 것이다. 

 

그런데 스프링에서 컴포넌트 스캔을 하면 이 과정을 스프링이 알아서 해 준다. 직접 할 필요가 없다는 것이다. 

 

컴포넌트 스캔하는 방법/과정

(1) 각 클래스에게 역할에 맞는 annotation을 붙인다. 

(2) 클래스 사이에 의존관계가 있는 경우가 있다. 

A 클래스가 B 클래스에게 의존한다고 해 보자. 이때 A 클래스는 자신의 생성자를 만들 때 B 클래스 타입의 매개변수를 받아 와서 new() 방식으로 B 클래스에게 의존한다. (뒤에서 다루겠지만 생성자를 만드는 것 말고 다른 의존 방법도 있다. )

위와 같은 기존 방식과 달리, 컴포넌트 스캔에서는 new()를 쓰지 않는다. 왜냐하면 객체의 생성을 스프링에서 관리하기 때문이다. 그냥 A 클래스의 생성자로 B 클래스 타입의 매개변수를 받는 것은 동일한데, 다른 점은 new()를 쓰지 않고 싱글턴 패턴으로 객체를 관리해 주며, 해당 생성자 위에 @Autowired이라는 annotation을 붙인다는 것이다. 

 

싱글턴 패턴을 쓰는 이유는 굳이 Service 등의 클래스를 여러 개 만들 필요가 없기 때문이다. 또한 여러 객체가 만들어지는 경우 그 안의 필드, 리소스들이 서로 동일하지 않게 되어 문제가 발생할 가능성도 커진다. 

 

[1] 컴포넌트 스캔 미사용

// service.java
public class Service{
	private Repository repository;
    
    public Service(Repository repository){
    	this.repository = repository;
    }
    
    // methods
    
    // example method
    public void example(){
    	service = new Service(new Repository());
    }
    
}

// repository.java
public class Repository{

	// methods

}

[2] 컴포넌트 스캔 사용

// service.java
@Service
public class Service{
	private Repository repository;
    
    @Autowired
    public Service(Repository repository){
    	this.repository = repository;
    }
    
    // methods...
}

// repository.java
@Repository
public class Repository{
	// methods...
}

 

 

의존관계 주입하는 여러 방법

의존관계 주입에는 크게 3가지 방법이 있다. (1) 생성자 주입, (2) 메소드(setter) 주입, (3) 필드 주입이다. 

그러나 생성자 주입이 편리하고 단점도 적어서(없는건지 적은건지 모르지만 셋 중에는 가장 좋다.) 주로 생성자 위에 @Autowired를 붙인다. 

 

메소드 주입을 잘 하지 않는 이유

메소드 주입은 setter 메소드의 매개변수로 의존하는 클래스 타입의 변수를 받아서 할당하는 방식이다. 그러나 보통 스프링 서버를 한번 로딩하고 나면 굳이 변수를 여러 번 할당하거나 바꿔 낄 일이 없다. (예를 들어서, 서비스 객체에 리포지토리를 여러 번 바꿔 끼울 일이 없다. ) 그래서 굳이 만들 필요가 없다. 또한 이런 메소드를 만들 때는 public으로 선언해야 하는데, 만일 다른 곳에서 setter 메소드를 잘못 사용하면 중간에 다른 리포지토리가 할당되는 등의 오류가 발생할 수 있기 때문에 권장하지 않는다. 

 

필드 주입을 잘 하지 않는 이유

중간에 바꿔 낄 수 없고, 인텔리제이 등의 IDE에서도 권장하지 않는다는 방식으로 밑줄이 그어진다. 


16. 자바 코드로 직접 스프링 빈 등록하기

 

그런데 컴포넌트 스캔을 사용하지 않고 직접 스프링 빈을 등록하는 방법이 있다. 

 

직접 등록하는 방법의 이점

-리포지토리 등 클래스를 '바꿔 낄 일'이 생길 때 필요하다. (ex. DB를 여러 개 사용하는 경우, 다른 DB로 변경하는 경우 등등)

 

컴포넌트 스캔과 비교했을 때의 단점

-컴포넌트 스캔 코드가 훨씬 간결하다. 

-또한 컴포넌트 스캔을 아예 안 쓸 수는 없다. @Service, @Repository annotation은 스프링 빈으로도 등록할 수 있지만, @Controller annotation은 사용해야만 스프링에서 컨트롤러로 인식할 수 있다. 

 

스프링 빈 등록하는 방법

(1) Config 클래스를 SpringBootApplication 클래스와 같은 디렉토리에 만든다. 

(2) 해당 클래스 위에 @Configuration annotation을 붙인다. -> 스프링에게 @Bean들을 찾을 때 여기서 찾으라고 표시해 주는 역할인 것 같다.

(3) 해당 클래스 안에다가는 @Bean을 사용해서 생성자들을 등록한다. 

 

예시 코드

@Configuration
public class Config{
	
    @Bean
    public Service service(){
    	return new Service(repository());
    }
    
    @Bean
    public Repository repository(){
    	return new Repository();
    }
    
}

 

'server-side > spring' 카테고리의 다른 글

spring 개발일지 21-22강  (0) 2022.03.08
spring 개발일지 18-19강  (0) 2022.03.08
spring 개발일지 10-12강 + 개발 고민주제 틈틈이 정리  (0) 2022.03.04
spring 개발일지 5-6강  (0) 2022.03.03
spring 개발일지 4강  (0) 2022.03.03

* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-04 #

 

#목차#

10-1. 일반적인 웹 어플리케이션 계층 구조

10-2. 비즈니스 요구사항 정리

11. 회원 도메인과 리포지토리 만들기

12. 회원 리포지토리 테스트 케이스 작성


10-1. 일반적인 웹 어플리케이션 계층 구조

 

(1) 컨트롤러(Controller)

웹 MVC의 컨트롤러 역할이다. 다만 나의 경우 MVC 구조가 아니라 REST API 구조로 개발을 진행할 것 같은데, 이 경우에도 컨트롤러라는 명칭과 역할이 유효한지는 아직 모르겠다. 

 

(2) 서비스(Service)

비즈니스 로직을 다루는 부분이다. (계좌 이체, 주문 발송 데이터 추가 등등) 아직 컨트롤러를 제외한 부분은 구현해본 적이 없기 때문에 방대한 웹 어플리케이션에서 서비스와 컨트롤러의 정확한 차이점은 잘 모르겠다. 비즈니스 로직을 구현하는 부분인 만큼 앞으로 제일 복잡해질 부분인 것 같다. 

 

(3) 리포지토리(Repository)

DB에 직접 접근하는 부분이다. DB에 접근하는 코드를 많이 써야할 것으로 예상된다. DB에 접근해서 도메인 객체를 추가/변경/삭제하는 작업이 직접적으로 일어나는 부분이다. 

 

(4) 도메인(Domain)

비즈니스 도메인 객체이다. 비즈니스 로직을 처리하기 위해 만드는 객체를 의미한다. 


10-2. 비즈니스 요구사항 정리 with 새로웠던 부분들

 

(1) DB 설계

강의에서는 아주 간단한 회원가입 형식을 구현할 수 있도록 DB를 설계할 예정이라고 한다. 그러나 내가 목표로 하는 프로젝트는 주문 관리, 배송, 장바구니 등등 다양한 기능이 포함된 비즈니스 로직을 갖췄기 때문에 추후 [스프링 활용 편-1] 강좌를 생각해볼 필요가 있겠다!

+ DB 설계는 누가 하는걸까? 메인 개발자가 하나? 다음에 이 부분을 물어봐야겠다. 

+ 메인 개발자가 확정되었는지, nodeJS 쓰는지 spring 쓰는지도 물어봐야지. 

 

(2) 리포지토리 인터페이스(Repository Interface)

구현 클래스가 아니다. 어떤 DB를 사용할지 확정되지 않은 상황에서 개발을 진행하거나, 여러 개의 DB와 연결해야 하는 상황에서는 인터페이스를 사용한다고 한다. 그래야 구현체를 비교적 쉽게 변경/추가할 수 있기 때문이다. 

+ 프로젝트에서는 어떤 DB를 사용할 예정인지(줌 미팅에서는 1차 개발에서는 DB를 하나만 사용할 거라고 들었다) 물어봐야겠다. 그러나 2차 개발을 한다면 어차피 여러개의 DB를 사용해야 하므로(실제로도 회원 정보 등을 여러 DB에 나눠서 저장하는 것이 꽤 흔하고, 이럴 경우 DB에서 오류 발생 시 어떤 DB/스키마에서 오류가 났는지를 쉽게 알 수 있다는 장점이 있다. <-> 단점은 복잡하다는 것), 리포지토리 인터페이스를 알아두면 언젠가는 분명 도움이 될 것 같다. 


11. 회원 도메인과 리포지토리 만들기

 

순서는 

[ 도메인 클래스 만들기 -> 리포지토리 인터페이스 만들기 -> 리포지토리 구현체(메모리ver) 만들기 ] 이 순서로 진행했다. 

 

1) 도메인 클래스 Member.java

크게 Long(Wrapper 클래스) 타입의 id 필드와 string 타입의 name 필드로만 이뤄져 있다. (로그인이 없는 초간단 구조...)

 

보통 회원가입을 진행하는 경우에도, 서버에서 회원을 구분하는 목적으로 id라는 필드를 선언한다. 즉 '로그인에서 사용자가 입력하는 아이디'와 '서버에서 사용자를 분류하거나 찾기 위한 목적의 아이디'는 서로 다르다. 여기서의 id 필드는 서버에서 구분하기 위한 목적의 필드이다. 

 

또한 실제 서비스에서는 아이디를 long이 아닌 AtomicLong 타입으로 주로 선언한다고 한다. 이유는 '동시성 문제' 때문이란다. (회원가입이 동시에 일어나는 경우 등등에서는 AtomicLong 타입이 문제를 발생시키지 않는다는 것 같다. Map의 경우에도 ConcurrentHahMap을 사용해야 동시성 문제가 발생할 가능성이 없다고 한다. )

 

회원 관련 정보이므로 필드들은 private 타입으로 설정하고, 대신에 getter, setter 메소드가 있다. 

 

2) 리포지토리 인터페이스 MemoryRepository.java

객체로 DB에 접근한다고 가정할 때 필요한 기본적인 메소드를 '선언'했다. (정의는 구현체에서 한다. 인터페이스는 선언만!) 

회원을 저장하는 save(), id로 회원을 찾는 findById(), name으로 회원을 찾는 findByName(), 등록된 모든 멤버를 보여주는 findAll() 메소드를 선언했다. 

 

다만 findById(), findByName() 메소드의 경우 찾는 회원이 없으면 null 값이 반환될 수 있다. 이 경우 null은 메소드의 리턴 타입인 Member과 달라서 오류가 날 수 있다. 

하지만 Java 8 이상에서는(맞나?) Optional<> 문법을 제공한다. Optional<타입>이란, 메소드가 해당 타입을 리턴할 수도 있고 null을 리턴할 수도 있을 때 사용한다. 이 경우에는 Optional<Member>를 사용해서, 주어진 정보에 해당하는 회원이 있으면 Member 타입, 없으면 null도 리턴될 수 있도록 선언했다. 

 

3) 리포지토리 구현체 MemoryMemberRepository.java

인터페이스에서 선언만 해놓은 메소드들을 override해서 재정의한다.

강의에서는 DB가 없는 상황을 가정했기 때문에, DB 대신으로 저장소 역할을 할 store라는 private static 이면서 Map 타입인 객체를 선언한다. Map 타입인 이유는 앞서 도메인에서 사용자를 구분하기 위한 목적으로 선언한 id 필드를 key 값으로 하고, member 객체를 value로 둬서 사용자를 조회하기 편리하게 만들기 위해서이다. 다만 Map은 구현체가 아니라 인터페이스이므로, HashMap 구현체를 사용한다. 

private static Map<Long, Member> store = new HashMap<>();

즉 이런 코드가 된다. 

+ HashMap을 사용할 거면 앞의 Map 부분 대신에 HashMap 타입으로 필드를 만들면 되지 않을까? 왜 Map 타입으로 선언했는지는 잘 모르겠다. (이 부분이 교재에 있었는데 찾아봐야겠다.)

 

실제로 findById()와 findByName() 메소드는 이 store 저장소 객체를 이용해서 사용자 정보를 조회한다. 

 

findById()의 경우 map의 key 값이므로 간단하게 get(key value) 메소드를 이용해서 조회가 가능하다.

그러나 findByName()의 경우는 매개변수로 주어지는 string name은 map의 key 값이 아니므로 바로 조회할 수 없다. 강의에서는 그래서 람다식을 사용했다. .stream(), .filter() 메소드를 통해서 매개변수로 주어진 name과 이름이 같은 멤버 객체를 조회하고 리턴하는 코드이다. 

 

save() 메소드의 경우 보통은 저장한 객체를 다시 리턴해준다. 

 

findAll() 메소드의 경우, 여러 객체를 한번에 리턴해야 하기 때문에 List 타입을 사용했다.

(정확히는 List 인터페이스를 받은 ArrayList 타입. 아까의 Map 경우랑 똑같다. 왜 앞 부분에도 ArrayList 타입이 아니라 List 타입으로 선언했는지를 잘 모르겠다. )


12. 회원 리포지토리 테스트 케이스 작성

 

앞서 스프링이 별도로 테스트를 위한 패키지와 라이브러리를 제공하는 만큼 테스트는 매우매우 중요하다고 언급했다. 따라서 11강에서 작성한 리포지토리 구현 클래스의 메소드들이 잘 작성되었는지를 테스트 해 보려고 한다. 

 

테스트의 장점은 (1) 메인 메소드를 매번 실행할 필요가 없고, (2) 수십개의 테스트를 한 번에 돌릴 수 있으며, (3) 실제 DB에 연결된 클래스 등을 테스트할 때 DB 등의 자료가 변경될 위험이 없는 등 여러 가지가 있다. 

+ 실제 DB를 연결하는 경우는 어떻게 테스트 코드를 짜는지 궁금하다. 임시 DB를 만드려나? 아니면 강의에서처럼 DB역할을 대신할 객체를 만드는 걸까?

 

테스트 코드는 src>test 디렉토리 내부에 작성하며, 여러 파일이 있을 경우 src>main 내부와 비슷한 구조로 작성한다. 강의에서는 리포지토리 클래스 하나만 테스트를 해 보았다. 또한 관용적으로 테스트 클래스의 이름은 테스트하려는 클래스의 이름 + Test 를 붙인다. 예를 들어, 여기서는 MemoryMemberRepositoryTest.java 파일이 작성된 셈이다. 

 

테스트 클래스 내부에서는 우선 테스트하려는 클래스 객체를 만든다. 하나의 테스트 클래스에서 여러 테스트 메소드를 작성할 수 있다. 이때 각 테스트 메소드 위에다 반드시 @Test annotation을 붙여야 한다. 그래야 스프링이 해당 메소드를 테스트로 인식한다. 

 

또한 테스트에서는 System.out.println() 등으로 화면에 직접 출력을 하지 않는다. 대신 Assertions 또는 assertThat 등의 외부 테스트 라이브러리의 메소드를 가져온다. 그래서 테스트를 실행해 보면 별도의 출력이 없다. 잘 실행되면 초록색 체크 표시가 뜨고, AssertThat 등의 조건과 맞지 않는 결과가 출력될 경우에는 에러가 뜬다. 

+ 강의에서는 항상 테스트 메소드의 맨 마지막에 Assertions 또는 AssertThat 코드가 배치되는데, 꼭 이 코드가 마지막에 배치되어야 하는지도 궁금하다. 

 

여러 개의 테스트 사이에는 반드시 순서나 의존관계가 없어야 한다. 스프링에서는 여러 개의 테스트가 있을 때 실행순서가 랜덤이다. 어느 순서로 테스트하던지에 상관없이, 에러가 발생하지 않아야 한다. (ex. 테스트 1의 결과에 따라서 테스트 2의 결과가 달라지는 경우 등등) 또한 DB 등 저장소 역할을 하는 객체에 접근하는 경우, 메모리를 초기화하지 않으면 이전 테스트에서 남긴 데이터/객체가 다음 테스트의 결과에 영향을 줄 수 있다. 따라서 하나의 테스트가 끝나고 나면 메모리를 비워주는 작업이 필요하다. 

 

스프링에서는 @AfterEach annotation을 붙인 메소드를 사용하면 각 테스트가 끝날 때마다 해당 메소드를 실행시킨다. 보통 이런 메소드에는 데이터 저장소의 초기화 코드가 들어간다. 

 

또한 예제 코드를 보니 테스트를 꼭 메소드 수 만큼 만들 필요는 없는 것 같다. 그냥 테스트하려는 부분에 초점을 맞춰서 필요한 만큼의 테스트를 만들면 될 것 같다. 

그리고 테스트 메소드는 외부에서 따로 매개변수를 받지 못하므로, 기존 메소드의 리턴 타입을 쓰기보다는 매개변수도 없고 리턴 타입도 void를 사용하는 것 같다. 

 

'server-side > spring' 카테고리의 다른 글

spring 개발일지 18-19강  (0) 2022.03.08
spring 개발일지 15-16강  (0) 2022.03.05
spring 개발일지 5-6강  (0) 2022.03.03
spring 개발일지 4강  (0) 2022.03.03
spring 개발일지 1-3강  (0) 2022.03.02

* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-03 #

 

#목차#

1. 스프링에서 뷰를 보여주는 원리

2. 스프링 구조의 기본인 컨트롤러 클래스

3. IDE가 아닌 터미널 창에서 스프링 프로젝트 실행하기

+ 예상 못 한 오류

+ 인상깊은 부분

+ 추가 공부할 부분


1. 스프링에서 뷰를 보여주는 원리

 

스프링에서 만든 뷰를 띄우려면 html 파일이 필요하다. html 파일은 resources>static 또는 resources>template 디렉토리에 저장한다. 그러나 두 디렉토리에 저장하는 것은 각각 다른 의미가 있다. 

 

1) resources>static

정적 파일(static files)들을 모아두는 디렉토리이다. 여기에는 정적 웹페이지도 포함된다. 별도의 코딩을 거치지 않고 그대로 html 파일을 띄우는 경우가 정적 웹페이지에 해당한다. 물론 여기에 assets, css, js 등의 디렉토리를 추가해서 css, js, 기타 jpg 파일 등을 추가하여 다른 종류의 정적 파일들도 저장할 수 있다. 

 

 

이 static 디렉토리를 이용하여 스프링에서 제공하는 기능이 있다. 바로 웰컴 페이지(Welcome Page) 기능이다. 

웰컴 페이지란, HTTP GET 메소드를 통해(주로 url 검색) 기본 주소(127.0.0.1:8080)로 들어갔을 때 나오는 페이지를 의미한다. 

 

웰컴 페이지를 설정하기 위해서는 resources>static 디렉토리에 index.html 파일을 추가하면 된다. HTTP GET 방법으로 기본 URL에 들어왔을 때(POST 등 다른 방식이면 웰컴 페이지 기능이 적용되지 않을 수 있다) index.html 파일이 웰컴 페이지로 사용된다. 

 

2) resources>template

여기에도 .html 파일을 저장할 수 있지만, 정적 파일이 아닌 파일을 저장한다. 말 그대로 일정의 양식을 따르는 '템플릿'으로 사용할 파일들을 저장한다. 사실 html 파일을 정적으로 사용하지 않고 여러 변수를 외부에서 받아오는 방법은 여러 가지가 있다. 그 중의 하나가 템플릿 엔진(template engine)이다. 

 

앞서 3강에서는 템플릿 엔진으로 사용할 Thymeleaf 라이브러리를 당겨 왔다. 파일의 기본 형식은 html과 큰 차이가 없지만, 이런 식으로 템플릿 엔진을 불러와서 'th'라는 변수로 설정하여 사용할 수 있다. 

<html xmlns:th="http://www.thymeleaf.org">

2. 컨트롤러(Controller class)

 

템플릿의 경우, 정적 파일과 달리 어떤 데이터를 입력하는지에 따라 뷰에서 출력되는 내용이 달라질 수 있다. Spring MVC(Model-View-Controller) 패턴에서는 뷰를 다루기 위해서 '컨트롤러' 클래스를 사용한다. 컨트롤러 클래스들은 보통 src>main 디렉토리 아래에 따로 controller 디렉토리를 설치하고 그 안에서 관리한다. 

 

컨트롤러의 특징

(1)

컨트롤러 클래스에는 반드시 @Controller 이라는 annotation(@)을 붙여 이 클래스가 컨트롤러로 사용됨을 명시해 주어야 한다. 그래야 스프링 내부에서 뷰를 처리할 때 해당 클래스의 메소드를 사용해서 사용자가 의도한 대로 뷰를 처리할 수 있다. 

 

(2)

컨트롤러 안에는 여러 메소드가 들어갈 수 있다. 메소드 위에도 @ annotation을 붙여서 해당 메소드의 역할을 명시해 주어야 한다. 

많이 사용되는 annotation 중 하나는 @GetMapping(url 주소)이다. 이게 붙은 메소드는 해당 url 주소로 HTTP GET 접근을 했을 때 어떤 식으로 뷰를 처리할지에 대한 내용을 담게 된다. 

( @GetMapping = @RequestMapping(method=RequestMethod.GET) )

 

(3)

해당 메소드는 매개변수로 Model(모델)을 받는다. MVC 중 하나인 모델은 외부에서 따로 컨트롤러에 할당하는 매개변수는 아니고, 스프링에서 자동으로 컨트롤러 클래스에 매개변수로 주입하는 것 같다. 

 

또한 모델에는 여러 메소드가 있다. 강의에서는 addAttribute() 메소드를 사용해서 모델에 속성을 부여했다. addAttribute 메소드는 '속성의 이름'과 '속성값'을 받는다. 

ex) addAttribute 메소드로 "data"라는 속성 이름과 "Hello" 라는 값을 모델의 속성으로 추가했다고 해 보자. 

 

(4)

컨트롤러는 뷰와 연결되며, 뷰를 리턴해야 한다. 이때 resource>template에 저장된 템플릿의 이름을 단순 문자열로 리턴한다면, 해당 뷰와 연결된다. 

ex) resource>template에 hello.html 파일이 있다고 해 보자. 컨트롤러 클래스에서 "hello"라는 문자열을 리턴했다면, 스프링은 자동으로 resource>template 에서 "hello"라는 이름의 템플릿 파일을 찾아서 뷰로 리턴한다.


3. IDE가 아닌 환경(터미널 창)에서 스프링 프로젝트 실행하기

 

앞서 프로젝트는 모두 인텔리제이 IDE에서 실행되었다. 그러나 사실 자바로 작성된 프로젝트의 경우, jar 파일이 있다면 로컬 터미널(윈도우의 cmd 창)에서도 실행할 수 있다. 물론 그러기 위해서는 몇 가지 과정이 필요하다. 

 

(1) 프로젝트 빌드

스프링 프로젝트가 설치된 디렉토리로 이동해서 프로젝트를 빌드해야 한다. 윈도우 cmd의 경우, cd 명령어를 통해 프로젝트의 디렉토리로 이동한다. 이후 gradlew.bat build 명령어를 입력하면 빌드가 실행되고, 프로젝트 내부에 build라는 새로운 디렉토리가 생긴다. 

 

(2) jar 파일 찾아서 실행

프로젝트에서 build>libs 디렉토리로 이동해서 dir 명령어를 통해 안에 어떤 파일이 있는지 본다.

빌드가 잘 실행되었다면 .jar 으로 끝나는 파일이 두 개가 있다. (하나만 있을 수도 있다.) 이 중 우리는 -plain이 아닌 그냥 jar 파일을 실행하면 된다. 

 

실행은 자바 환경변수를 이용해서 java -jar 프로젝트의-이름

이렇게 명령어를 입력하면 IDE 환경에서와 똑같이 스프링 시작화면이 뜬다. 그러면 성공이다. 이런 방식으로, 만약 다른 환경에서 스프링을 실행해야 할 경우가 생긴다면 jar 파일만 갖고 있으면 된다. 그러면 터미널에서 실행할 때도 필요한 라이브러리 등을 자동으로 설치해서 실행이 된다고 한다. 

 

+ 예상하지 못한 오류

실행은 잘 되었고 정적 파일로 설정한 웰컴 페이지도 그대로 나왔다. 그러나 앞서 사용했던 템플릿 엔진을 이용한 hello.html 파일이 제대로 실행되지 않았다. cmd 창에서 템플릿 엔진이 제대로 로딩되지 않았다는 오류가 떴다. 이유를 알아봐야겠다....


인상깊은 내용

 

스프링에서 모르는 내용을 검색하는 방법.

스프링은 너무 방대해서 모든 걸 다 알 수가 없다고 한다. 그래서 '모든 걸 알아야지!' 라고 접근하는 대신, 모르는 것을 찾는 방법을 아는 게 중요하다고 말씀하셨다. 

 

스프링 기본 홈(Spring | Home)화면으로 들어가서, Projects 탭에서 사용하는 스프링 라이브러리 이름을 누르면 해당 라이브러리에 대한 공식 문서 및 설명을 볼 수 있다. 이 강의에서는 Spring Boot를 사용했으므로 Projects>Spring Boot으로 들어가자. 이후 현재 사용하는 스프링 버전 옆의 'Reference Doc' 를 클릭하면 해당 버전에서 사용되는 기능들의 공식 문서들을 볼 수 있다. 


+ 추가로 알아볼 부분

1) 템플릿 엔진의 개념과 역할

2) spring REST와 spring MVC의 개념, 그리고 둘의 차이

3) 터미널 환경에서 실행했을 때의 템플릿 엔진 적용/로딩 오류

 

'server-side > spring' 카테고리의 다른 글

spring 개발일지 15-16강  (0) 2022.03.05
spring 개발일지 10-12강 + 개발 고민주제 틈틈이 정리  (0) 2022.03.04
spring 개발일지 4강  (0) 2022.03.03
spring 개발일지 1-3강  (0) 2022.03.02
spring 개발일지 #0  (0) 2022.03.01

* 이 포스트는 인프런에 있는 김영한 님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 들으면서 내용을 정리한 글입니다. *

 

[메인 컨텐츠]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

 

# 2022-03-03 #

 

#목차#

1. gradle에서 외부 라이브러리를 당겨오는 원리

2. gradle에서 당겨 오는 라이브러리의 종류

+ 몰랐던 개념들 / 더 알아볼 개념들


1. gradle에서 외부 라이브러리를 당겨오는 원리

 

gradle은 빌드 관리 도구로, 스프링에서 필요한 라이브러리를 당겨 온다.

그런데 스프링의 라이브러리들은 서로 의존관계가 있다. gradle은 외부 라이브러리를 당겨올 때 내가 지정한 라이브러리 뿐만 아니라, 그 라이브러리가 의존하는 다른 라이브러리까지도 모두 당겨온다. 그래야 그 라이브러리를 사용할 수 있다. 결과적으로 당겨오다 보면 spring core 라이브러리까지 모두 당겨오게 된다. 즉 처음에 선택한 라이브러리 당겨오기->그 라이브러리가 의존하는 다른 라이브러리 당겨오기-> ... 더 이상 의존하는 라이브러리가 없을 때까지 이런 방식으로 필요한 모든 라이브러리를 당겨온다. 그렇기 때문에 내가 필요하다고 지정한 라이브러리에 비해서 실제로 당겨온 라이브러리가 훨씬 많다. 

 

그래서 어떤 라이브러리를 당겨 왔는지를 보려면 Intellij 화면 왼쪽의 프로젝트 탭에서 External Libraries를 보면 된다. 또는 화면 우측 상단에 마우스를 대면 Gradle이라는 버튼/탭이 나오는데, 그 탭을 누르면 스프링이 의존관계에 기반해서 어떤 라이브러리를 당겨 왔는지를 순차적으로 볼 수 있다. 


2. gradle에서 당겨 오는 라이브러리의 종류

 

이 프로젝트에서는 어떤 라이브러리를 당겨 왔을까?

너무 많아서 간단하게 3가지만 다뤘다. 

 

1) 내장형 웹 서버 Tomcat

 

톰캣(tomcat)은 웹 서버이다. 스프링같은 프레임워크가 발달하기 전에는 톰캣을 따로 설치하고, 그 안에 자바 코드를 밀어넣는 방식으로 웹 서비스를 만들었다고 한다. 그러나 스프링에서는 내장형 웹 서버로 톰캣을 사용할 수 있는 라이브러리를 만들었다. 따라서 이 라이브러리를 당겨오기만 하면(설치하기만 하면) 톰캣을 따로 설치하는 과정이 없이도 웹 서버를 실행할 수 있게 바뀌었다고 한다. 

 

스프링 3강에서 웹 서버를 실행할 때, 톰캣 서버는 HelloSpringApplication.java 의 메인 메소드를 실행하면 바로 실행되었다. 즉 스프링에서 당겨오는 라이브러리와 상관 없이 내장형 톰캣 서버 라이브러리(정확한 명칭은 모른다)는 항상 설치된다는 말이다.

= 이 내장형 톰캣 서버 라이브러리는 기본적으로 당겨오는 라이브러리로, spring boot 라이브러리가 의존하고 있는 라이브러리이다. 

 

+ 내장형 웹 서버의 개념이 생소하고 웹 서버의 개념도 생소하다... 개념을 좀 더 알아보도록 하자. 

 

 

2) 로깅(Logging) 라이브러리

 

[참고한 포스트]

로깅에 대하여 (tistory.com)

SLF4J 이용하여 로그 남기는 방법 (with Logback) (tistory.com)

 

로깅이란 말 그대로 로그를 남기는 것이다. 로그는 웹 서버에서 남기는 일종의 기록이다. 개발자들은 System.out.println()을 쓰는 대신 로그를 남긴다고 한다. 그 이유는 로그의 레벨(WARN, INFO, FATAL 등등등)에 따라서 로그를 모아 보기도 쉽고, 외부 파일에 저장하기도 쉽기 때문이다. 반면 println() 메소드는 계속 사용한다면 프로그램 실행 속도를 저하시킨다는 문제도 있기 때문에 로깅 라이브러리가 사용된다. 

 

로그 레벨은 TRACE-DEBUG-INFO-WARN-ERROR 으로 5개의 레벨이 있다. 오른쪽으로 갈수록 심각성이 크다. 로깅 라이브러리를 사용하면 원하는 레벨의 로그만 모아서 볼 수 있으며, 로그의 근원지에 따라서 다른 기준치의 로그 레벨을 사용할 수도 있다. 

ex) A 클래스에서 발생하는 로그는 INFO 레벨 이상(INFO 및 INFO보다 심각성이 높은 레벨인 WARN, ERROR)만 출력하고, B 클래스에서 발생하는 로그는 ERROR 레벨만 출력하도록 구체적인 로깅 설정을 할 수 있다. 

 

 

아무튼 강의에서는 크게 2가지의 로깅 라이브러리를 언급했다. 물론 이거 말고도 여러 라이브러리가 있다. 

 

(1) slf4j

: 로그를 남길 때 어떤 구현체로 남길지를 선언한 인터페이스라고 한다. 즉 로그를 직접 남기는 라이브러리는 아니지만, 어떤 구현체를 사용해서 로그를 남길 때 어떤 방법으로 남길지를 선언한 라이브러리다. 로깅 인터페이스를 사용하면 나중에 더 좋은 로깅 구현체가 생겨서 구현체 라이브러리를 교체하더라도, 따로 코드를 변경하지 않아도 된다는 장점이 있다. 

 

(2) logback

: 로그를 남기는 라이브러리이며, slf4j와 달리 인터페이스가 아니라 구현체이다. 또한 

 

또한 여러 로깅 라이브러리 중에서 스프링이 기본적으로 사용하는 로깅 라이브러리가 정해져 있다. Apache의 commons-logging 라이브러리인데, 이 경우 다른 로깅 라이브러리를 당겨와도 기본 라이브러리가 있어서 사용되지 않을 수 있다. 이런 경우는 logback 라이브러리를 기본으로 사용하면서도 commons-logging 라이브러리와 똑같은 구조를 가진 jcl-over-slf4j 라이브러리를 당겨오면 된다고 한다. 로깅 라이브러리를 사용할 때도 고려할 점이 많은 것 같다.

 

 

3) 테스트 라이브러리

 

앞서 3강에서는 프로젝트 기본 구조에서 main 디렉토리 말고 test 디렉토리가 따로 생성되어 있다고 했다. 이만큼 테스트의 중요도가 크다. 따라서 gradle에서도 기본 설정으로 테스트 라이브러리를 당겨온다. 정확한 테스트 과정은 모르지만, 대표적인 몇 개의 테스트 라이브러리를 살펴보자. 

 

(1) jUnit

가장 많이 사용되는 테스트 라이브러리이다. 

 

(2) mockito, assertj

테스트를 간편하게 실행할 수 있도록 도와주는 라이브러리이다. 

 

(3) spring-test

테스트 라이브러리들을 스프링 환경에서 바로 실행할 수 있도록 스프링 환경과 연결해 주는 라이브러리이다. 


+ 몰랐던 개념들 / 다음에 더 알아볼 개념들

1. 웹 서버의 개념, 내장형 웹 서버의 개념, tomcat은 구체적으로 무엇인지

2. 로깅의 실제 활용 방법, 로깅 인터페이스와 로깅 구현체 라이브러리는 구체적으로 어떻게 구성되었는지

3. 테스트 라이브러리에서는 어떤 기능을 제공하며, 테스트는 어떤 순서대로 실행되는지

 

'server-side > spring' 카테고리의 다른 글

spring 개발일지 15-16강  (0) 2022.03.05
spring 개발일지 10-12강 + 개발 고민주제 틈틈이 정리  (0) 2022.03.04
spring 개발일지 5-6강  (0) 2022.03.03
spring 개발일지 1-3강  (0) 2022.03.02
spring 개발일지 #0  (0) 2022.03.01

Date: 2022-03-01 

 

[강의]

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의 (inflearn.com)

(스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 v2021-12-01)

 

스프링 강의로 유명한 김영한 님의 스프링 기초 강의이다. 심지어 무료(!)

이 강의를 들으면서 스프링으로 간단한 프로젝트를 구현하며 스프링 기초를 이해해 보자.


1. 스프링 시작하기 및 기본 개념

 

Spring Boot:

spring 프로젝트를 쉽게 만들어주는 도구. start.spring.io 주소로 이동해서 빌드 관리 도구, spring boot 버전, 사용할 언어(여기서는 Java), 프로젝트 이름, 사용할 Dependencies(스프링이 제공하는 여러 기능, 라이브러리들)을 고르고 GENERATE 버튼을 누르면 해당 조건들을 갖춘 프로젝트를 바로 만들어 준다. 

 

빌드 관리 도구:

maven or gradle.

Maven이 Gradle보다 먼저 나왔지만 현재는 gradle을 많이 사용한다. 여기서도 gradle으로 진행한다. 

 

빌드 관리 도구란: 스프링 프로젝트를 만들면서 필요한 외부의 라이브러리 등의 사용을 관리해 주고, 프로젝트를 실행(빌드)하는 과정을 전반적으로 관리해주는 프로그램이다. (외부 라이브러리를 사용할 때는 gradle에 명시해 주면 프로젝트 내부에서 따로 import 과정을 거치지 않아도 되고, gradle 자체에서 build 설정이 되어 있기 때문에 사용자는 그냥 프로젝트를 실행하기만 하면 된다.)

 

Dependencies: 스프링에서 제공하는 여러 라이브러리(기능들) 중 어느 것을 사용할 것인지를 선택. 

여기서는 (1) 웹 프로젝트이므로 Spring Web, (2) .html 파일을 만들기 위해서 필요한 템플릿 엔진(template engine) 중 하나인 thymeleaf 두 개의 라이브러리를 추가했다. (템플릿 엔진의 개념은 추후에 자세히 알 수 있을 것 같다.)


2. 프로젝트 기본 구조

 

프로젝트의 기본 구조는 아래 화면과 같다. 

그 중에서 강의에서 짧게 설명한 부분만 정리해 보았다. 

 

-.idea : 인텔리제이에서 프로그램을 실행할 때 사용하는 파일들

 

-.gradle: gradle에서 사용하는 파일들

 

-src: 사용자가 직접 작성하는 파일들. 이 안에 프로그램을 구성할 파일들이 들어간다. 

-src>main: 스프링 프로젝트를 구성하는 파일들.

-src>main>java: 스프링 프로젝트를 구성하는 자바 파일들.

-src>main>resources: 스프링 프로젝트를 구성하는, 자바 파일이 아닌 다른 파일들(html 템플릿 등이 포함된다.)

 

-src>test: 스프링 프로젝트를 구성하는 테스트 코드. 별도의 디렉토리로 분류될 정도로 요새는 test의 중요성이 커졌다고 한다. 

 

-.gitignore: git을 사용한 사람들이라면 알던데 나는 잘은 모른다. git으로 프로젝트를 업로드할 때는 소스 코드만 올리고 기본 설정 파일 등 부가적인 것들은 올리지 말아야 하는데, 이 작업을 도와주는 파일이다. 간단하게 보면 프로젝트 안의 파일들 중 업로드할 것과 그렇지 않은 것을 구분해 준다. 

 

-build.gradle: gradle을 통해 프로젝트를 build할 때 이 파일을 참고한다. 아까 start.spring.io에서 선택한 dependencies들이 이 파일의 plugins{}과 dependencies{} 내부에 명시되어 있다.

dependencies{} 안에 명시된 라이브러리들은 repositories{} 안에 명시된 mavenCentral() 이라는 곳에서 가져온다고 한다. 이 상태는 가장 기본일 뿐, 만약 가져오고 싶은 외부 라이브러리가 있다면 mavenCentral() 외에 다른 곳을 추가할 수도 있다.

또한 스프링에서는 테스트 코드를 위한 별도의 라이브러리(JUnit)도 기본으로 설정되어 나온다. 이만큼 테스트가 중요한 것 같다. 


3. 프로젝트 실행하기

 

실행 방법은 매우 간단하다. src>main>java>HelloSpringApplication 안의 메인 메소드를 실행하면 프로젝트가 실행된다.

이게 가능한 이유는 해당 HelloSpringApplication 클래스에는 @SpringBootApplication 이라는 Annotation(@)이 붙어 있고, 이 Annotation 내부에는 Tomcat 서버가 내장되어 있기 때문에, 메인 메소드를 실행하면 tomcat 서버가 실행되는 원리이다. 

+ 처음에는 아무런 작업도 하지 않았기 때문에 Error Page가 나오는 것이 정상이라고 한다.


+ 빌드 빠르게 하는 방법

gradle은 빌드 관리 도구라서 빌드 과정이 쉽게 이뤄지도록 도와주지만, gradle을 거쳐서 빌드를 하지 않아도 된다. 오히려 Intellij에서 gradle을 거치지 않고 빌드를 할 경우 속도가 더 빠르다.

 

Windows의 경우, File>Setting에서 설정 창을 열고 Build, Execution, Development>Gradle 창을 연다. 

그리고 'build and run using' 과 'run tests' 옵션을 gradle이 아니라 Intellij로 설정한다. 

그러면 빌드 속도가 좀 더 빨라진다. 

'server-side > spring' 카테고리의 다른 글

spring 개발일지 15-16강  (0) 2022.03.05
spring 개발일지 10-12강 + 개발 고민주제 틈틈이 정리  (0) 2022.03.04
spring 개발일지 5-6강  (0) 2022.03.03
spring 개발일지 4강  (0) 2022.03.03
spring 개발일지 #0  (0) 2022.03.01

+ Recent posts