Spring, Apache, Java

Spring Boot를 사용하는 이유

cwchoiit 2024. 5. 30. 11:03
728x90
반응형
SMALL

가장 중요한 건 Spring BootSpring Framework와 다른게 아니다. 그저 Spring Framework를 편리하게 사용할 수 있게 기본셋과 자동화 된 작업들이 매우 많이 있을 뿐이다. 

 

Spring Boot가 어떤걸 대신 해줄까? 대표적인 것들은 다음과 같다.

 

  • 톰캣 내장 서버
  • 최적의 라이브러리 버전 관리
    • 손쉬운 빌드 구성을 위한 스타터 종속성 제공
    • 스프링과 외부 라이브러리의 버전을 자동으로 관리 (예를 들어, 스프링 부트 3.0은 A라는 외부 라이브러리의 이 버전과 잘 어울려요! 하고 알아서 맞춰 준다)
  • 자동 구성: 프로젝트 시작에 필요한 스프링과 외부 라이브러리의 빈을 자동 등록해준다.
  • 프로덕션 준비: 모니터링을 위한 메트릭, 상태 확인 기능 제공

정말 여러모로 개발자 대신 많은 것들을 해준다. 그리고 이제 스프링 부트가 없는 전으로는 돌아갈수도 없다. 즉, 게임체인져라는 소리다.

그럼 진짜 스프링 부트가 없던 시절 스프링 프레임워크로 개발하고 서버에 띄우려면 어떻게 해야했을까? 이 과정을 스프링부트가 없던 시절로 돌아가서 직접 해보고 아 이런 불편함을 해결해 주는구나를 직접 느껴보자.

 

저걸 다 해볼 필요는 없고 톰캣을 직접 설치하고, 직접 실행하고, WAR 파일을 직접 배포해서 서버를 띄우는 작업을 해보자.

 

톰캣 설치

WAS의 대표적인 톰캣을 직접 PC에 설치해보자.

공식 사이트

 

Apache Tomcat® - Welcome!

The Apache Tomcat® software is an open source implementation of the Jakarta Servlet, Jakarta Pages, Jakarta Expression Language, Jakarta WebSocket, Jakarta Annotations and Jakarta Authentication specifications. These specifications are part of the Jakarta

tomcat.apache.org

들어가면 좌측에 다운로드 섹션이 있다. 거기에 가장 최신 버전으로 설치를 해보자. (현재는 10이 가장 최신 버전이고 11은 아직 Alpha 버전)

 

.zip 파일을 내려받고 압축을 풀면된다. 풀면 다음 스텝을 거쳐야한다.

1. 톰캣설치폴더/bin 이동

2. chmod 755 *

3. ./startup.sh (톰캣 실행) (중지는 ./shutdown.sh)

 

그럼 톰캣이 실행되어야 정상이다. 그리고 웹 브라우저에 다음 경로를 입력해본다. http://localhost:8080

 

정상적으로 톰캣 화면이 보여지면 잘 띄워진 것. 만약, 제대로 띄워지지 않은 경우엔 로그 파일을 확인해봐야 한다.

로그 경로: 톰캣설치폴더/logs/catalina.out

 

설치가 끝났으니까 톰캣 서버에 띄울 우리만의 웹 서버를 만들어야 한다. 한번 만들어보고 직접 설치한 톰캣위에 올려보자.

 

프로젝트 설정

build.gradle

plugins {
    id 'java'
    id 'war'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'

    implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
}

test {
    useJUnitPlatform()
}
  • 아무것도 없는 상태로 프로젝트를 생성한 다음에 build.gradle 파일만을 설정한다.
  • plugins에 id 'war'를 넣어줘야 이 프로젝트를 빌드할 때 'war' 파일로 만들어준다.
  • 스프링 없이 서블릿을 사용해서 간단하게 웹 서버를 만들어 볼 것이기 때문에 implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0' 이 부분을 추가해준다.

 

TestServlet

package org.example.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

/**
 * <a href="http://localhost:8080/test">http://localhost:8080/test</a>
 * */
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("TestServlet.service");

        resp.getWriter().println("TestServlet.service");
    }
}
  • 기본 패키지 경로 안에 servlet이라는 패키지를 하나 만들고 TestServlet 클래스를 만든다.
  • 이 클래스는 서블릿이다. 그래서 HttpServlet을 상속받고 간단하게 로그를 출력하고 응답으로 "TestServlet.service"를 찍어주고 끝난다.
  • 그래서 이 서블릿의 경로는 http://localhost:8080/test 이다.

 

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
index.html
</body>
</html>
  • java, resources 와 같은 레벨에 webapp이라는 폴더를 만들고 그 폴더안에 이 index.html 파일을 추가한다.
  • 이 파일은 웰컴 페이지가 된다.

 

이렇게 만들고 나면 이제 테스트 해보기 위해 이 프로젝트를 빌드한다. 터미널에 이 프로젝트 루트 경로에 가서 다음 명령어를 입력한다.

./gradlew build

 

정상적으로 수행이 되면, 프로젝트 루트 경로에 build 라는 폴더가 하나 생긴다. 그 안에 libs 폴더로 들어가면 .war 파일이 보일것이다.

그 파일 한번 압축을 풀어보자. 어떻게 보이는지 한번 보고 가면 좀 이해가 잘 된다.

jar -xvf yourwarfile.war

 

압축을 풀면 다음과 같이 보여질 것이다.

  • index.html: webapp 폴더에 만든 index.html 파일이다.
  • META-INF: html, css과 같은 정적 파일들이 포함된다.
  • WEB-INF: 자바 파일을 컴파일 한 .class파일들이 있고 사용하는 외부 라이브러리(.jar)파일이 보관된다.

이게 .war 파일이 생겨먹은 모습이다. 이것을 톰캣 서버에 배포하면 이제 웹 서버를 띄울 수 있는것이다.

한번 톰캣에 배포해보자.

 

1. 우선 톰캣설치폴더/webapps 경로로 가야한다.

2. 기본으로 들어있는 것들을 다 삭제한다.

3. 이 경로에 위 .war 파일을 복사한다.

4. 톰캣 서버를 내렸다가 다시 띄운다.

5. http://localhost:8080 으로 접속해본다.

 

그럼 다음과 같이 우리가 만든 웰컴 페이지가 보여진다.

서블릿도 호출해보자.

 

그리고 서블릿을 호출할 때 찍었던 로그를 catalina.out 파일에서 확인해보자.

 

 

그리고 만약 로컬에서 작업하고 다시 톰캣에 배포하고 이 반복 작업이 너무 귀찮으니까 IntelliJ에서 이 작업을 대신 해주는데 어떻게 하는지 알아보자. (유료 버전이니까 유료 버전이 아니라면 패쓰!)

 

Run > Edit Configurations

 

좌측 상단 + 버튼 클릭

 

'tomcat'이라고 검색하면 Tomcat Server 아래 Local 클릭

 

하단 Application server를 설정해주면 되는데, 톰캣 서버가 깔린 루트 경로를 지정해주면 된다.

 

Deployment 탭 > + 버튼 > Artifact 

 

둘 중 아무거나 선택해도 되는데 (exploded)로 선택

 

Application context는 꼭 아무것도 없는걸로 비워야 한다. 

 

이렇게 설정하고 Apply > Run

 

서버가 잘 띄워진다.

중간 결론

이렇게 해서 톰캣을 설치하고 만든 프로젝트를 war로 패키징해서 배포하고 웹 서버를 띄울 수 있다. 상당히 번거로운 이 작업을 스프링 부트가 대신 해줬던 것이다. 그러니까 스프링 부트 없던 시절로는 돌아갈 수가 없는것이다. 

 

이 뿐이 아니라 버전 관리를 용이하게 해주고, 효율적인 모니터링 제공이나 개발자 대신 해주는 빈 등록과 같은 여러 작업들이 스프링 부트를 사용할 수 밖에 없게 한다. 

 

자동구성

자동 구성이란, 스프링 부트가 개발자 대신 이것저것 여러가지를 빈으로 자동 등록해주는데, 이 덕분에 개발 생산성은 극대화되고 안정성도 높아진다. 가장 대표적으로 스프링 부트가 자동 구성을 해주는 빈은 `DataSource`인데 데이터베이스와 연동하기 위해서 반드시 필요한 정보들을 가지고 있는 클래스이다. 그래서 `DataSourceAutoConfiguration`이라는 클래스를 찾아보면 다음과 같이 생겨먹었다.

사진을 보면, 가장 상단에 @AutoConfiguration 애노테이션이 보인다. 이게 자동으로 빈으로 등록하겠다는 애노테이션이라고 보면 된다.

저 하나하나의 애노테이션이 지금 당장은 중요하지 않지만, 이렇듯 스프링 부트가 대신 빈으로 등록해준다는 것을 이해하고 넘어가면 된다.

 

참고로, 저 @AutoConfiguration의 `before = {SqlInitializationAutoConfiguration.class}`는 자동구성을 하려면 SqlInitializationAutoConfiguration 이라는 클래스가 그 전에 먼저 자동 구성이 되어야 한다는 사전 조건이라고 생각하면 된다.

 

그리고 @ConditionalOnClass 애노테이션은 조건이다. 간단하게 설명해서 저 애노테이션에 들어간 클래스들이 등록이 되어 있어야만 자동 구성으로 구성이 될 것을 허락하는 애노테이션. 이건 직접 해보면 더 좋다.

 

예를 들어, 현재 시스템의 메모리 상태를 알려주는 클래스가 있다고 가정해보자. 

Memory

package memory;

public class Memory {
    private long used;
    private long max;

    public Memory(long used, long max) {
        this.used = used;
        this.max = max;
    }

    public long getUsed() {
        return used;
    }

    public long getMax() {
        return max;
    }

    @Override
    public String toString() {
        return "Memory{" +
                "used=" + used +
                ", max=" + max +
                '}';
    }
}

 

MemoryFinder

package memory;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MemoryFinder {

    public Memory get() {
        long max = Runtime.getRuntime().maxMemory();
        long total = Runtime.getRuntime().totalMemory();
        long free = Runtime.getRuntime().freeMemory();
        long used = total - free;

        return new Memory(used, max);
    }

    @PostConstruct
    public void init() {
        log.info("MemoryFinder init");
    }
}

 

MemoryController

package memory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
public class MemoryController {

    private final MemoryFinder memoryFinder;

    @GetMapping("/memory")
    public Memory system() {
        Memory memory = memoryFinder.get();
        log.info("System memory: {}", memory);

        return memory;
    }
}

이렇게 컨트롤러를 통해 `/memory` 로 들어가면 메모리를 상태를 알려주는 편의 기능을 만들었다고 생각해보고, 이 기능은 직접 만든게 아니라 외부 라이브러리라고 가정해보자. (그래서 일부러 패키지도 스프링 부트 메인이 있는 패키지가 아니라 다른 패키지로 만들었다.)

 

MemoryConfig

package hello.config;

import memory.MemoryCondition;
import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MemoryConfig {

    @Bean
    public MemoryController memoryController() {
        return new MemoryController(memoryFinder());
    }

    @Bean
    public MemoryFinder memoryFinder() {
        return new MemoryFinder();
    }
}

패키지가 스프링 부트의 메인 클래스 밖에 있으므로 이 메모리 관련 기능을 빈으로 등록하자.

 

메모리 상태가 잘 보인다. 근데, 이 메모리 정보를 운영 서버와 개발 서버를 분리했을 때 개발 서버에서만 동작하도록 설정하고 싶을 수 있다. 아니면 이런 특정 조건일 땐 보여주고 특정 조건이 아닐 땐 보여주지 않고 싶은 이런 경우, 이럴때 @Conditional 애노테이션을 사용해서 빈으로 등록할지 아닐지를 결정할 수 있다. (@ConditionalOnClass 애노테이션이 곧 @Conditional 이라고 생각하면 된다)

 

우선 그러려면, 스프링 부트가 제공해주는 Condition 이라는 인터페이스를 구현하는 클래스를 만들어야 한다.

MemoryCondition

package memory;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

@Slf4j
public class MemoryCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // Jar 실행 중 옵션을 다음과 같이 줄 때: -Dmemory=on
        String memory = context.getEnvironment().getProperty("memory");
        log.info("memory = {}", memory);
        return "on".equals(memory);
    }
}

Condition이라는 인터페이스는 matches()를 구현해야 하는데, 이 메서드가 참인 경우 @Conditional 애노테이션이 빈으로 등록하게 허락해주고, 거짓인 경우 @Conditional 애노테이션이 빈으로 등록하지 못하게 막아준다. 그럼 이 클래스를 어디서 사용하면 될까? 그렇다. 아까 빈으로 등록하기 위해 만들었던 MemoryConfig에 붙여주면 된다.

 

MemoryConfig

package hello.config;

import memory.MemoryCondition;
import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
@Conditional(MemoryCondition.class) // 여기!
public class MemoryConfig {

    @Bean
    public MemoryController memoryController() {
        return new MemoryController(memoryFinder());
    }

    @Bean
    public MemoryFinder memoryFinder() {
        return new MemoryFinder();
    }
}

이렇게 하면, MemoryCondition 이라는 클래스가 가진 matches()가 참 또는 거짓인 경우에 따라 이 MemoryConfig가 스프링 부트에 의해 구성이 될지 아닐지를 결정하게 된다. 어디서 많이 본 모양이다. 그렇다. 스프링 부트가 자동 구성을 해주는 DataSourceAutoConfiguration도 아까 이렇게 비슷하게 생겼었다. 

 

실제로 그런지 확인해보자.

 

JVM 옵션을 주지 않았을 때

JVM 옵션을 줬을 때

 

이렇게 스프링 부트는 자동 구성할 때 조건을 통해 자동 구성 빈이 등록될지 아닐지도 알아서 해준다. 어쩔때 그럴까? 개발자가 직접 빈을 등록할때가 그렇다. 그게 아니더라도 이 @Conditional은 빈을 등록할 때 유용하게 사용할 수 있어보이니 잘 기억해두자.

 

근데! 이거보다 훨씬 더 간단하게 스프링 부트가 제공해주는 것이 있다. 

@ConditionalOnProperty(name = "memory", havingValue = "on")

이렇게 @ConditionalOnProperty 애노테이션으로 똑같이 JVM 옵션이 다음 조건과 같으면 이 구성정보를 등록하겠다는 애노테이션.

 

MemoryConfig

package hello.config;

import memory.MemoryCondition;
import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
// @Conditional(MemoryCondition.class)
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryConfig {

    @Bean
    public MemoryController memoryController() {
        return new MemoryController(memoryFinder());
    }

    @Bean
    public MemoryFinder memoryFinder() {
        return new MemoryFinder();
    }
}
728x90
반응형
LIST