기본적인 Spring Boot의 흐름을 정리하기 위한 목적으로 글을 작성했습니다. 의존성 주입이 어떻게 이루어지고, 생성자 주입이 왜 권장되는지, Entity 클래스를 응답 객체로 넘기면 왜 안되는지 등의 내용은 다른 포스팅에서 정리할 예정이므로 참고해 주시면 감사드립니다.
Spring Boot 환경 설정
start.spring.io에서 프로젝트 파일을 받아서 사용하는 것이기 때문에, intelliJ에서 바로 프로젝트를 생성하는 것이 편리합니다.
intelliJ version - 2024.1.4(Ultimate Edition) / Community version도 무관합니다.
Java 버전은 21이상을 사용하는 것을 권고합니다.
build.gradle
외부 라이브러리와, 스프링 부트의 버전, 자바 버전 등을 관리하는 파일입니다. 해당 파일에서 필요한 라이브러리 의존성을 주입하세요.
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.6-SNAPSHOT'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.groupname'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/snapshot' }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' // 웹 프로젝트
implementation 'org.projectlombok:lombok' // lombok
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // jpa
runtimeOnly 'com.h2database:h2' // h2 database : 테스트용/개발용 으로만 사용시 runtimeOnly 사용
implementation 'com.mysql:mysql-connector-j' // mysql 운영 DB
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
Application.yaml
spring:
application:
name: project_name
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.H2Dialect
h2:
console:
enabled: true
IntelliJ Gradle Compile -> Java Direct Compile
- ( File -> Setting ) Preferences -> Build, Execution, Deployment -> Build Tools Gradle
- Build and run using: IntelliJ IDEA
- Run tests using: IntelliJ IDEA
build and run
Mac
./gradlew build
cd build/libs
java -jar [builded java file name].jar 실행 확인
Window
./gradlew 내부에 있는 gradlew.bat 실행
gradlew build
H2 DB 확인해보기
SpringBoot Application이 정상적으로 실행이 됐다면, 아래 주소로 접속하십시오
http://localhost:8080/h2-console
이후 application.yaml 파일에 명시한 정보를 기입해 인증하면, in-memory DB인 h2를 사용할 준비가 됐습니다.
요청에서 응답까지
Tomcat ( WAS )
클라이언트의 Request는 URL을 통해 Tomcat에 도달하고, 해당 정보를 바탕으로 HttpServletRequest 객체를 만듭니다.
Tomcat 내부의 filter 혹은 Tomcat filter interface를 구현한 Spring Boot Servlet filter에서 인증/인가 처리를 마친 뒤, 요청을 다음 단계로 넘깁니다.
Dispatcher Servlet ( Spring Boot )
클라이언트의 요청을 처리할 수 있는 Handler를 조회하고, 그에 적절한 처리를 할 수 있는 HandlerAdapter를 조회합니다.
Handler (Controller)에 보내기 전, 전처리 작업이 필요한 경우 preHandle를 호출하기도 합니다.
Controller ( Handler )
여러 분기 작업이 발생합니다. 우선 해당 컨트롤러가 @ResponseBody를 가지고 있는지 여부를 판단합니다.
HttpServletResponse 객체의 body field에 값을 넣어주어야 한다면, body에 실리는 데이터의 종류에 따라, StringHMC 혹은 MappingJackson2HMC가 객체를 Serialization 해줍니다.
만약 HttpServletResponse 객체가 body field에 값을 넣을 필요가 없다면, 제공할 수 있는 View가 있는지 확인합니다.
있다면 ViewResolver가 resources/templates/${returned string}.html에 해당하는 View를 찾아서 Dispatcher Servlet에게 응답 값을 리턴해줍니다. 만일 없다면 해당 이름의 정적 자원이 있는지 확인한 뒤에, 해당 값을 리턴해줍니다.
만약 Controller의 이러한 작업에서 Exception이 발생한다면, Spring Interceptor가 이를 확인하고 요청 흐름에 intercept를 발생시키고 Dispatcher servlet에게 해당 내용을 알립니다.
Web Application Hierarchy Architecture
Model을 사용하는 구조는 자주 사용하지 않으니, @RestController 기준으로 설명해 보겠습니다.
아래 코드 내용은 가장 기본적인 동작을 보여주기 위한 코드이므로 권장되는 패턴이 아니므로 사용에 주의해 주세요.
기본적으로, @Controller는 MVC 패턴에 적절한 응답을 할 수 있도록 구성돼 있습니다.
위에서 설명한 ViewResolver, resource/static 등의 자원을 사용할 수 있고 특수한 경우 @ResponseBody를 통해 HttpServletResponse의 body에 값을 넣어 응답할 수도 있습니다.
하지만 대부분의 경우 클라이언트에게 Json Data를 넘겨주는 것이 대부분이기 때문에 @Controller + @ResponseBody인 @RestController를 사용합니다.
Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
private final MemberService memberService;
@PostMapping()
public ResponseEntity<?> registryMember(@RequestBody MemberDto.registryMemberDto registryMemberDto) {
memberService.registryMember(registryMemberDto);
return ResponseEntity.ok(null);
}
@GetMapping()
public ResponseEntity<?> getMembers() {
return ResponseEntity.ok(memberService.getMembers());
}
@GetMapping("/{memberId}")
public ResponseEntity<?> getMember(@PathVariable("memberId") Long memberId) {
return ResponseEntity.ok(memberService.getMember(memberId));
}
}
Service
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member getMember(Long id) {
return memberRepository.findById(id).orElse(null);
}
public List<Member> getMembers() {
return memberRepository.findAll();
}
public void registryMember(MemberDto.registryMemberDto registryMemberDto) {
memberRepository.save(
Member.builder()
.name(registryMemberDto.name())
.email(registryMemberDto.email())
.password(registryMemberDto.password())
.build()
);
}
}
Repository
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}
Entity
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "Members")
@Builder
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long memberId;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "password", nullable = false)
private String password;
}
DTO ( Data Transfer Object )
public class MemberDto {
public record registryMemberDto(String name, String email, String password) {}
}