프레임워크 사용 이유, 그리고 그 여러 프레임워크 중 왜 하필 스프링을 사용하는 이유를 배운다.
스프링 사용 이유다. 스프링 부트 사용 이유가 아니다.
프레임워크를 사용하는 이유는
그러나 단점도 있다.
보통 이 장단점 사이에서 저울질하며 프레임워크를 사용할지 안할지를 결정한다.
Spring은 2003년에 등장했다. 당시에는 JEE (Java Enterprise Edition)가 막 유행하기 시작했으며, 애플리케이션 개발이 많이 번거로웠던 편.
주된 목표는 JAva를 위한 IoC container이 되는 것이다. IoC container이 뭔지는 다른 글에서 다루겠다.
Spring 이코시스템의 가장 핵심적인 영역은 당연히 Spring 프레임워크 자체다. 이것은 또 여러 모듈로 나뉘어진다. 크게 다음과 같이 나눌 수 있다.
Core : 스프링과 관련된 핵심 기능들 제공. DI, AOP, internationalization, validation 등을 지원한다.
Data Access : 데이터 접근과 관련된 기능을 제공. 여러 방식을 제공하는데 JTA, JPA, JDBC로 접근하는 방식을 지원한다. 대표적인 Spring Data JPA의 경우 JPA를 더 쉽게 활용할 수 있도록 돕는 모듈이다.
Web : Servlet API (by Spring MVC) / Reactive API(by Spring WebFlux), WebSocket, WebClient, STOMP등의 웹과 관련된 기능을 전부 제공한다.
Integration : 애플리케이션을 만들고 나면 이를 기업 구조 자체에 통합시키는 작업이 필요하다. 그 과정에서 통신이 필요할텐데 이와 관련된 기술인 JMS, JMX, RMI등을 제공한다.
Test : 유닛/통합 테스트를 하는데 필요한 Mock Object, Test Fixture, Context Management, 캐싱 등을 제공한다.
이 중 몇몇을 언급하자면
위의 내용들을 처음 보면 너무 방대하다 느낄 수 있는데, 추후 글들에서 천천히 다뤄보도록 하겠다.
보통 Spring initializr을 쓰는데... 사이트로 제공이 되지만 필자는 IntelliJ를 활용했다.
IntelliJ에서 Spring Boot 프로젝트를 만들겠다고 하면 원하는 dependency (starter)을 바로 추가하는 것이 가능하다. 이 프로젝트에서는 Web, JPA, H2, Security를 추가했다.
상호작용할 Data를 나타내는 class를 만들어보자. 이것은 Spring 문법을 활용하지 않고, JPA 문법을 활용한다.
아마 아는 사람도 있을텐데 JPA는 Java 객체랑 DB 사이를 어떻게 매핑 및 관계를 형성하는지에 대한 '명시'다. 즉 실제로 우리가 사용하는 것들은 JPA에서 지정한 약속들을 지키는 프레임워크들이다. (Hibernate, EclipseLInk)
Spring은 여기에 한층 더 나아가, 위의 JPA를 구현한 프레임워크들 위에 하나의 층을 더 쌓았고 그것이 Spring Data, 정확히는 Spring Data JPA다. 목적은 앞에서 말했듯, JPA 구현 프레임워크들을 더 쉽게 사용할 수 있도록 하기 위해서다. JPA, JPA 구현체, Spring Data JPA에 대한 더 자세한 얘기는 이 글 참고.
뭐 이런 관계는 알겠다. 그러면 Spring에서 JPA를 사용할려면 대체 어떻게 해야 하는가? 먼저 밑과 같은 Entity Class를 만든다. @Entity
로 annotate된 class가 Entity class다. 이 class의 목표는 DB에 실제로 있는 Table 내의 각 record를 Java에서 어떤 형태의 class(의 객체)로 생각할지를 알려주는 것이다.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
private String firstName;
@NotNull
private String lastName;
// Standard constructor, getters and setters
}
- 여기서 유의할게 있는데,
@NotNull
이라는 annotation을 사용하려면 Jakarta Bean Validation API를 사용해야 한다. maven이든 gradle이든 이 extension을 추가하도록 하자.- 또 이미 어느정도 공부한 분들은,
@Table
이 class에 필요하지 않냐고 할 수 있다. 사실 그게 명시되어 있지 않아도 알아서 DB의 table과 mapping을 하는게 가능하다. 이 때 mapping되는 table은 class의 camelcase 이름에 대응되는 underscore 이름을 가지는 table. 뭐 애초에@Entity
가 table을 생성하는데 사용되기도 하지만 이건 나중 이야기.
타 annotation에 대한 설명이나 주석 부분에 들어가야 할 코드는 나중에 Spring Data 공부를 하면서 차차 알아보도록 하자. 뭔지 대충 감은 오겠지만 말이다. 사실 이번 글에서는 저것의 정의가 그렇게 중요한건 아니다. 저 Entity를 위한 JPA repository를 얼마나 손쉽게 만들수 있냐가 중요한 것이다.
Spring은 Repository
라는 인터페이스를 제공한다. 이는 어떤 Entity랑 연관된 Table을 접근하는데 사용된다. 여기서 JPA를 위한 Repository
, JpaRepository
라는 것이 존재한다. 이 녀석은 JPA랑 관련된 모든 method들을 제공하는 인터페이스다. 이 때문에 JPA 중 몇몇 기능만 사용하는 경우 좀 과하다.
우리는 CrudRepository
라는 인터페이스를 사용할 것이다. CRUD (Create, Read, Update, Delete)랑 관련된 method만 가진 인터페이스다. 다음과 같이 코드를 만들어보자.
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface EmployeeRepository
extends CrudRepository<Employee, Long> {
List<Employee> findAll();
}
이렇게 짜면 일단 우리는 이 우리만의 Repository
인 EmployeeRepository
에 기본적인 CRUD operation을 하는게 가능하다. 왜냐? CrudRepository
를 extend 했으니까.
그러면 findAll
은 뭐냐. 일단 이름을 보면 Table에 있는 모든 Employee
를 구하라는 것을 유추하는게 가능하다. 그러면 정의는? 이건 SpringJPA에서 알아서 짠다. 이게 놀라온 부분이다. DAO 구현 없이 이게 가능하며, 어떻게 이름만 가지고 이를 파악하는지 궁금하면 이 글이랑 이 공식 문서를 확인해보자.
여하튼 이게 Spring의 무시무시한 점 중 하나다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class EmployeeController {
@Autowired
private EmployeeRepository repository;
@GetMapping("/employees")
public List<Employee> getEmployees() {
return repository.findAll();
}
// Other CRUD endpoints handlers
}
Employee를 추가하거나 삭제하는 operation에 대해서는 막고 싶다고 해보자. 그러면 해당 request에 대해서는 authentication이 이루어져도 관련 작업을 하지 못하도록 막아야 한다. 또 직원 정보 자체를 인증된 사람만 볼 수 있게 하고 싶다고 해보자. 이 때 Spring Security가 등장한다.
Baeldung의 코드는 deprecate된게 많으며, 같은 효과를 내려면 밑과 같이 작성해야 한다.
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers(HttpMethod.GET, "/employees", "/employees/**")
.permitAll()
.anyRequest()
.authenticated()
);
return http.build();
}
// other necessary beans and definitions
}
/employees
나 /employees/**
에 대해서 허용하고 나머지는 안되며, 저 요청마저도 인증된 경우에만 허용한다.@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class EmployeeControllerTests {
@Autowired
private MockMvc mvc;
@Test
@WithMockUser()
public void givenNoEmployee_whenCreateEmployee_thenEmployeeCreated() throws Exception {
mvc.perform(post("/employees").content(
new ObjectMapper().writeValueAsString(new Employee("First", "Last"))
.with(csrf()))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status()
.isCreated())
.andExpect(jsonPath("$.firstName", is("First")))
.andExpect(jsonPath("$.lastName", is("Last")));
}
// other tests as necessary
}
마지막으로 Spring Application은 따로 servlet container을 형성한 다음에 거기에 배포를 하는게 아니라 임베드된 Tomcat Server에다가 실행하는 것이 가능하다. (따로 관련 확장자를 미리 넣긴 해야 한다. 어려운건 아니지만.)
그러면 밑과 같은 코드로 실행이 가능하다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
사용감 : 개발자가 쉽게, 본인이 만들어야 하는 프로젝트와 관련된 환경설정을 하고 개발을 시작하는게 가능하다. 특히 Spring Boot가 이 장점을 더 증진시킴.
모듈성 : 스프링 프레임워크 전체를 꼭 넣을 필요 없고, 필요한 모듈들만 넣어서 프로젝트 개발이 가능. 심지어 타 프레임워크랑 같이 사용하는 것도 가능
다양한 표준 형태 준수 : Jakarta EE라고 들어보았는가? Java EE가 이클립스 재단에 넘어가면서 바뀐 이름이다. Java SE가 단순 프로그램 개발용 플랫폼인데, 여기에 서버 개발과 관련된 내용물을 추가한 것이 Jakarta EE다. Spring의 경우 이 Jakarta EE를 전부는 아니지만 꽤 많이 따르고 있다. 특히 기술 부분은 전부 따르고 있으며, 표준 요구사항보다 더 발전한 형태로 지원을 하는경우도 많다. 게다가 타 산업체 명시사항들도 따르는 것들이 몇개 있는 등 다양한 표준 형태를 가진다는게 장점이다.
테스트 관련 : 앞에서 봤듯이 테스트를 위한 환경도 금방 설정해주기 때문에 TDD와 같은 개발 방식에 매우 최적화되어 있다. 게다가 사용하는 오브젝트 대부분이 POJO라 테스트 만드는것도 크게 꼬이기가 힘들고, 설령 시뮬레이션하기 힘든 테스트들도 mock object를 활용해서 미믹하는게 가능함.
성숙도 : 엄청 오랬동안 발전한 프레임워크다. 그래서 어지간한 문제, 특히 흔히 있는 문제들은 최적으로 해결하는게 가능함. 새로운 언어 기능에 대한 지원 등의 발전 환경도 매우 잘 되어 있음
커뮤니티 : 오픈 소스다. 그리고 이 덕분에 많은 개발자들이 이 프레임워크를 어떻게든 유지 보수 및 발전시키고 있음.
배우는게 꽤 힘들다. 앞에 있는 예제도 매우 간단하지만 생략한 개념들이 매우 많으며 스프링에서 추구하는 디자인 철학까지 공부해야 하다보니 매우 빡세다. 구체적인 아키텍처까지 공부한다면? 어휴. 근데 순수하게 구현하는것도 빡세서 별 상관이 없는것 같다.
어쨌든 추가 자원을 소모하는 것이라 최적화가 매우 중요한 프로젝트에서는 기피된다.