@Data
public class TestDto {
// 테스트 하고싶은 Annotation으로 변경 가능
@NotBlank
private String stringField;
@NotNull
@Range(min = 1, max = 9999)
private Integer integerField;
}
import static org.assertj.core.api.Assertions.assertThat;
public class BeanValidationTest {
@Test
void beanValidation() {
// Spring과 통합하면 아래 두줄의 코드는 사용하지 않는다.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// Test 하고싶은 상황을 만들어서 검증 가능
TestDto dto = new TestDto();
dto.setStringField(" ");
dto.setIntegerField(1);
// DTO를 검증
Set<ConstraintViolation<TestDto>> violations = validator.validate(dto);
// 검증 결과가 예상대로 발생했는지 확인
// 검증에 걸린 필드가 있어야 함
assertThat(violations).isNotEmpty();
// 2개의 제약 위반 발생
assertThat(violations.size()).isEqualTo(2);
// Validation에 걸린 내역을 출력
for(ConstraintViolation<TestDto> violation : violations) {
// 아래의 결과에 Message가 있으면 Validation에 걸린것.
// Default Message가 있기 때문에 출력됨
// Message를 수정하고싶다면 Annotation 속성값(message="입력")으로 설정할 수 있다.
// 결과가 비어있으면 Validation에 걸리지 않은것.
System.out.println("violation = " + violation.getMessage());
}
}
}
import가 org.hibernate.validator로 시작하면 하이버네이트를 사용할 때만 제공되는 검증 기능으로 다른 구현체로 validator를 교체하였을 경우 동작하지 않는다. 하지만 org.hibernate.validator를 대부분 사용한다.
Validator
📌 유효성 검증(Validation) 인터페이스입니다. 특정 객체에 대한 데이터 유효성을 검사하고, 유효하지 않은 경우 오류 메시지를 생성하여 처리할 수 있도록 설계되었습니다. 이 인터페이스는 커스텀 유효성 검증 로직을 구현하거나, 스프링의 유효성 검증 프레임워크와 통합할 때 사용됩니다.
단순히 Annotation을 선언해주면 검증이 완료되는 이유는Validator(Validation을 사용하는것)가 존재하기 때문이다.
@Valid 는 JAVA 표준이고 @Validated 는 Spring 에서 제공하는 Annotation이다.
@Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.
@Valid 는 MethodArgumentNotValidException 예외를 발생시킨다.
@Validated 는 ConstraintViolationException 예외를 발생시킨다.
Validator 적용
Validator 적용 전
@ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도
성공 : Controller 정상 호출
실패 : TypeMismatch FieldError 발생
Validator 적용 후
@ModelAttribute → 각 필드 바인딩 → 성공한 필드만 Bean Validation 적용
Integer 타입 필드에 문자가 오면 애초에 검증의 의미가 없다.
성공 : String 필드에 문자입력 → 바인딩 성공 → String 필드에 Bean Validation 적용
실패 : Integer 필드에 문자입력 → 바인딩 실패 → bindingResult에 TypeMismatch FieldError 추가 → 바인딩에 실패한 필드는 값이 없음(null) → Bean Validation 적용하지 않음
Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다. 바인딩(변환)에 성공한 필드만이 Bean Validation을 적용하는 의미가 있다.
에러 메세지
📌 Spring의 Bean Validation은 Default로 제공하는 Message들이 존재하고 임의로 수정할 수 있다
Error Message
Bean Validation을 적용하고 BindingResult에 등록된 검증 오류를 확인해보면 오류가 Annotation 이름으로 등록되어 있다.
@Data
public class TestDto {
@NotBlank
private String stringField;
@NotNull
@Range(min = 1, max = 9999)
private Integer integerField;
}
@Slf4j
@RestController
public class BeanValidationController {
@PostMapping("/error-message")
public String beanValidation(
@Validated @ModelAttribute TestDto dto,
BindingResult bindingResult
) {
// bindingResult Field Error 출력
if (bindingResult.hasErrors()) {
return String.valueOf(bindingResult.getFieldError());
}
// 성공시 문자열 반환
return "회원가입 성공";
}
}
Postman
integerField 는 비워두고 stringField 만 값(”문자열”) 입력
출력결과
Field error in object 'testDto' on field 'integerField': rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [널이어서는 안됩니다]
NotNull 오류 코드를 기반으로 MessageCodesResolver 를 통해 메세지 코드 생성
Spring 에서는 오류 메시지 코드관리를 위해 MessageCodesResolver 인터페이스의 구현체인 DefaultMessageCodesResolver를 기본으로 사용한다.
에러 메세지 수정하기
NotNull.Object.fieldName
Annotation의 message 속성 사용
@Data
public class TestDto {
@NotBlank(message = "메세지 수정 가능")
private String stringField;
}
NotNull.fieldName(MessageSource)
필드명에 맞춘 사용자 정의 Message
NotNull.FieldType(MessageSource)
필드 타입에 맞춘 사용자 정의 Message
NotNull
Annotation 자체에 포함된 Default Message
네가지 방법중 Annotation의 message 속성을 사용하여 에러 메세지를 수정하면 됩니다.
MessageSource란 Spring에서 지원하는 인터페이스로 메세지의 국제화를 위해 사용된다.
Object Error
필드 단위가 아닌 객체 전체에 대한 오류를 나타낸다. 예를들어 두 필드 간의 관계를 검증할 때 ObjectError를 통해 해당 오류를 BindingResult에 기록할 수 있다.
지금까지 위에서 배운 내용은 객체가 아닌 필드 단위를 검증해서 발생하는 Field Error에 대한 내용
@ScriptAssert
코드예시
비밀번호와 비밀번호 확인 필드가 동일한지 검증
@Data
@ScriptAssert(lang = "javascript", script = "_this.password === _this.confirmPassword", message = "Passwords do not match")
public class UserRegistrationDto {
@NotBlank(message = "Password is required")
private String password;
@NotBlank(message = "Confirm password is required")
private String confirmPassword;
}
실제로 @ScriptAssert는 제약사항 때문에 사용하지 않는다.
실무에서는 훨씬 복잡한 Validation들이 필요하지만 대응이 불가능하다.
ex) 다른 객체끼리의 비교 혹은 DB조회 결과와 비교
Object Error의 경우 Java 코드로 직접 Validation 한다.
Java 코드로 구현하기
요구사항 : 총 구매 가격이 10000원 이상이여야 한다.
price * count ≥ 10000
@Getter
@AllArgsConstructor
public class OrderRequestDto {
@NotNull
@Range(min = 1000)
private Integer price;
@NotNull
@Range(min = 1)
private Integer count;
}
@Slf4j
@RestController
public class BeanValidationController {
@PostMapping("/object-error")
public String objectError(
@Validated @ModelAttribute OrderRequestDto requestDto,
BindingResult bindingResult
) {
// 합이 10000원 이상인지 확인
int result = requestDto.getPrice() * requestDto.getCount();
if (result < 10000) {
// Object Error
bindingResult.reject("totalMin", new Object[]{10000, result}, "총 합이 10000 이상이어야 합니다.");
}
// Error가 있으면 출력
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return bindingResult.getAllErrors().get(0).getDefaultMessage();
}
// 성공로직 ...
return "성공";
}
}
Object Error는 로직으로 구현하면 된다.
Postman
Bean Validation의 충돌
📌 등록, 수정 API에서 각각 다른 Validation이 적용된다면?
요구사항
상품
id (식별자)
name (이름)
price (가격)
count (재고)
상품 등록 API
식별자 값은 필수가 아니다.
name은 null, “”, “ “을 허용하지 않는다.
price는 10 ~ 10000 사이의 숫자로 생성한다.
count는 1 ~ 999 사이의 숫자로 생성한다.
상품 수정 API
식별자 값이 필수이다.
name은 null, “”, “ “을 허용하지 않는다.
price는 무제한으로 허용한다.
count는 1 ~ 999 사이의 숫자로 생성한다.
일반적으로 수정 API를 만들 때 식별자 id값을 항상 Controller에서 받도록 구성한다. HTTP 요청은 사용자가 임의로 변경하여 요청할 수 있음으로 항상 서버에서 최종적으로 추가 검증을 진행 해야한다. ex) 게시글 수정시 요청자 본인이 쓴 글인지 확인한다.
Product 저장 API
@Data
public class ProductRequestDto {
// 식별자는 Database에서 자동생성
@NotBlank
private String name;
@NotNull
@Range(min = 10, max = 10000)
private Integer price;
@NotNull
@Range(min = 1, max = 999)
private Integer count;
}
@Slf4j
@RestController
public class ConflictValidationController {
@PostMapping("/product")
public String save(
@Validated @ModelAttribute ProductRequestDto requestDto
) {
log.info("생성 API가 호출 되었습니다.");
// Validation 성공시 repository 저장로직 호출
return "상품 생성이 완료되었습니다";
}
}
Product 수정 API
@Data
public class ProductRequestDto {
@NotBlank
private String name;
// price 무제한 요구사항 반영
@NotNull
private Integer price;
@NotNull
@Range(min = 1, max = 999)
private Integer count;
}
@Slf4j
@RestController
public class ConflictValidationController {
@PutMapping("/product/{id}")
public String update(
@PathVariable Long id,
@Validated @ModelAttribute ProductRequestDto test
) {
log.info("수정 API가 호출 되었습니다.");
// Validation 성공시 repository 수정로직 호출
return "상품 수정이 완료되었습니다.";
}
}
@PathVariable의 required 속성의 기본값은 true이다.
해결방법
저장할 Object를 직접 사용하지 않고 SaveRequestDto, UpdateRequestDto 따로 사용한다.
Bean Validation의 groups 기능을 사용한다.
groups
Bean Validation의 groups 속성은 다양한 유효성 검사 시나리오를 정의할 때 사용된다. 동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 groups를 활용할 수 있다.
// 저장용 group
public interface SaveCheck {
}
// 수정용 group
public interface UpdateCheck {
}
@Data
public class ProductRequestDtoV2 {
// 저장, 수정 @NotBlank Validation 적용
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String name;
// 사용하는 모든곳에서 @NotNull Validation 적용
@NotNull
// 저장만 @Range 반영
@Range(min = 10, max = 10000, groups = SaveCheck.class)
private Integer price;
@NotNull
@Range(min = 1, max = 999)
private Integer count;
}
@Slf4j
@RestController
public class ProductController {
@PostMapping("/v2/product")
public String save(
// 저장 속성값 설정
@Validated(SaveCheck.class) @ModelAttribute ProductRequestDtoV2 requestDtoV2
) {
log.info("생성 API가 호출 되었습니다.");
// Validation 성공시 repository 저장로직 호출
return "상품 생성이 완료되었습니다";
}
@PutMapping("/v2/product/{id}")
public String update(
@PathVariable Long id,
// 수정 속성값 설정
@Validated(UpdateCheck.class) @ModelAttribute ProductRequestDto test
) {
log.info("수정 API가 호출 되었습니다.");
// Validation 성공시 repository 수정로직 호출
return "상품 수정이 완료되었습니다.";
}
}
groups VS DTO 분리
📌 Bean Validation의 충돌이 발생하는 경우 대부분 DTO를 분리하는 방법이 적절하다.
groups VS DTO 분리
groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용된다.
가독성이 떨어지고 코드 복잡도가 올라간다.
실무에서는 등록 폼과 수정 폼 자체를 분리해서 사용하기 때문에 DTO 분리 방법을 사용하면 된다.
단, 네이밍은 일관성있게 작성해야 한다.(SaveRequestDto, UpdateRequestDto)
DTO 분리
실제로 간단한 프로젝트를 개발해보면 저장, 수정시 Request가 비슷한 경우가 있다.
각각의 장단점이 존재하지만 어설프게 하나로 합칠 경우 유지보수시 엄청난 경험을 할 수 있다.
RequestDto가 변한다는건 해당 API의 스펙 자체가 변경되어 많은 수정이 발생한다.
실무에서는 거의 발생하지 않는 경우기 때문에 간단한게 아니라면 대부분 분리하도록 하자!
@Validated VS @Valid
@Validated
속성값이 존재한다.
spring이 제공하는 Annotation
@Valid
속성값이 존재하지 않는다, groups 기능 지원하지 않는다.
groups 기능을 사용하려면 @Validated를 사용해야 한다.
Java 표준 Annotation
@ModelAttribute, @RequestBody
📌 @Valid, @Validated는 @ModelAttribute뿐만 아니라 @RequestBody에도 적용할 수 있다. @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용하고 @RequestBody 는 HTTP Body Data를 Object로 변환할 때 사용한다.
@RequestBody 적용
@Data
public class ExampleRequestDto {
@NotBlank
private String field1;
@NotNull
@Range(min = 1, max = 150)
private Integer field2;
}
@Slf4j
@RestController
public class RequestBodyController {
@PostMapping("/example")
public Object save(
@Validated @RequestBody ExampleRequestDto dto,
BindingResult bindingResult
) {
log.info("RequestBody Controller 호출");
if(bindingResult.hasErrors()) {
log.info("validation errors={}", bindingResult);
// Field, Object Error 모두 JSON으로 반환
return bindingResult.getAllErrors();
}
// 성공 시 RequestDto 반환(의미 없음)
return dto;
}
}
Rest API 요청의 세가지 경우의 수
성공 요청: 성공
Controller 정상 호출
응답 반환
실패 요청: JSON을 객체로 변환하는 것 자체가 실패
field2에 String 입력
JSON → Object 변환 실패
중요! Controller가 호출되지 않는다.
반드시 JSON → Object로 변환이 되어야 Validation이 진행된다.
검증 오류 요청: JSON을 객체로 변환하는 것은 성공, 검증에서 실패
field2의 값에 범위를 넘어서는 값 입력 @Range(max = 150)
bindingResult.getAllErrors() 가 MessageConverter에 의해 JSON으로 변환되어 반환된다.
Controller를 실제로 호출한다.
log로 작성한 bindingResult error들이 콘솔에 출력된다.
정리
@ModelAttribute와 @RequestBody 차이점
@ModelAttribute
각각의 필드 단위로 바인딩한다.
특정 필드 바인딩이 실패하여도 나머지 필드는 정상적으로 검증 처리할 수 있다.
특정필드 변환 실패
컨트롤러 호출, 나머지 필드 Validation 적용
@RequestBody
필드별로 적용되는것이 아니라 객체 단위로 적용된다.
MessageConverter가 정상적으로 동작하여 Object로 변환하여야 Validation이 동작한다.
특정필드 변환 실패
컨트롤러 미호출, Validation 미적용
추가내용
bindingResult.getAllErrors()는 FieldError와 ObjectError 모두 반환한다.
Spring은 MessageConverter를 이용해 Error 객체들을 변환하여 응답한다.
RequestDTO 의 경우, 생성, 수정, 삭제, 모두 비슷하게 생겼어도 따로 분리해서 사용하자.
작성한 코드는 예시일 뿐 실제로는 API Spec에 맞는 응답을 만들어 클라이언트에 전달 해야한다.
📌 애플리케이션을 세 가지 주요 계층으로 나누어 구조화하는 방법으로 각 계층은 특정한 책임을 갖고 있으며, 계층 간에는 명확한 역할 분담이 이루어져 코드의 재사용성, 유지보수성, 확장성을 높이는 데 도움을 준다.
주요 특징
계층 분리:
시스템을 기능별로 분리하여 모듈화.
책임 분리:
각 계층은 고유한 책임과 역할을 가짐.
상호 의존성:
상위 계층은 하위 계층에만 의존하며, 계층 간의 의존성을 제한.
유지보수 용이:
특정 계층의 변경이 다른 계층에 최소한의 영향을 미침.
Layerd Architecture 개요
기존의 MVC 패턴에서 Controller는 역할이 무수히 많다.
요청에 대한 처리
예외처리
View Template 응답 or Data 응답
비지니스 로직 처리
DB 상호작용
문제점
Controller에서 요청에 대한 모든 처리를 수행한다. 즉, 책임이 너무 많다.
기능 추가, 수정, 삭제 등의 유지보수가 힘들어진다.
코드의 재사용성이 떨어진다. 메서드로 분리하여도 메서드를 호출하는 중복 코드가 발생한다.
Layered Architecture 구조
Presentation Layer
사용자의 요청을 받고 응답하는 역할을 수행한다.
화면을 응답하거나 데이터를 응답하는 API를 정의한다.
Business Layer(Service Layer)
비지니스 로직을 수행한다.
요청을 해석하여 Repository Layer에 전달한다.
일반적으로 하나의 비지니스 로직은 하나의 트랜잭션으로 동작한다.
Data Access Layer(Repository Layer)
데이터베이스와 연동되어 실제 데이터를 관리한다.
용어 설명
DTO(Data Transfer Object)
계층간 데이터 전달을 위해 사용되는 객체이다.
Model
Entity
추후 숙련주차에 배울 JPA와 관련이 있다.
JPA에서는 Entity라는 형태로 데이터를 반환한다.
DAO(Data Access Object)
계층 간 의존성
상위 계층은 하위 계층에만 의존합니다.
계층 간의 의존성을 단방향으로 제한하여 결합도를 낮춥니다.
계층 간 의존성 예시
Presentation → Application → Domain → Data Access
장점
유지보수성:
계층별 역할이 분리되어, 특정 계층의 변경이 다른 계층에 미치는 영향을 최소화.
재사용성:
서비스나 데이터 계층을 다른 애플리케이션에서도 재사용 가능.
테스트 용이성:
계층 단위로 테스트가 가능하여, 단위 테스트와 통합 테스트를 쉽게 수행.
확장성:
특정 계층에 새로운 기능을 추가하거나 변경하기 쉬움.
단점
복잡성 증가:
계층 간의 통신 코드로 인해 초기 개발이 복잡해질 수 있음.
성능 문제:
계층 간 호출이 많아지면 성능 저하 가능.
단순 CRUD에 과한 구조:
작은 애플리케이션에서는 계층형 아키텍처가 불필요한 복잡성을 초래.
계층형 아키텍처를 사용할 때 적합한 경우
복잡한 비즈니스 로직:
다양한 데이터 소스와 복잡한 비즈니스 규칙이 있는 애플리케이션.
협업 프로젝트:
역할 분리가 명확해 팀 간 작업이 효율적.
대규모 애플리케이션:
변경 사항을 쉽게 관리하고, 시스템 확장이 필요한 경우.
Layered Architecture 적용
1. Controller
클라이언트의 요청을 받는 역할을 수행한다.
요청에 대한 처리를 Service Layer에 전달한다.
Service에서 처리 완료된 결과를 클라이언트에 응답한다.
사용하는 Annotation : @Controller, @RestController
2. Service
사용자의 요청 사항을 처리한다.
DB와 상호작용이 필요한 경우, Repository Layer에게 요청한다.
사용하는 Annotation: @Service
3. Repository
DB와 상호작용을 수행한다.
Connection 연결, 해제
CRUD 작업 처리
사용하는 Annotation: @Repository
4. DTO(Data Transfer Object)
계층간 데이터 전달을 위해 사용된다.
요청 데이터를 처리하는 객체는 일반적으로 RequestDto로 명명한다.
응답 데이터를 처리하는 객체는 일반적으로 ResponseDto로 명명한다.
DTO (Data Transfer Object)
📌 애플리케이션에서 계층 간 데이터 전송을 목적으로 사용하는 단순한 객체입니다.
주로 데이터를 전송하기 위한 속성과 Getter/Setter 메서드만 포함하며, 비즈니스 로직을 포함하지 않습니다.
DTO의 주요 역할
데이터 캡슐화:
데이터 구조를 명확히 정의하여 계층 간 데이터 교환을 표준화.
안전한 데이터 전달:
데이터 모델(Entity)과 직접 연관되지 않아, 민감한 데이터 보호 가능.
데이터 변환 및 제한:
클라이언트가 필요로 하는 데이터만 전달하도록 제한.
API 설계에 유용:
RESTful API에서 응답(Response) 또는 요청(Request) 데이터를 정의.
DTO의 구조
DTO는 일반적으로 다음과 같은 형태로 작성됩니다:
속성 필드
Getter/Setter 메서드
(선택적) 생성자, toString(), equals(), hashCode()
public class UserDTO {
private Long id;
private String name;
private String email;
// 기본 생성자
public UserDTO() {}
// 생성자
public UserDTO(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getter와 Setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
DTO의 사용 예
a. 요청(Request) DTO
클라이언트가 서버에 데이터를 보낼 때 사용.
Request DTO 예제
public class CreateUserRequest {
private String name;
private String email;
// Getter와 Setter
}
Controller
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@RequestBody CreateUserRequest request) {
// 요청 데이터 처리
return ResponseEntity.ok("User created: " + request.getName());
}
}
public class UserResponse {
private Long id;
private String name;
private String email;
public UserResponse(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getter만 제공
}
Controller
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
// 서비스에서 가져온 데이터
UserResponse response = new UserResponse(id, "John Doe", "john.doe@example.com");
return ResponseEntity.ok(response);
}
}
@Service
public class UserService {
public UserResponse getUserResponse(UserEntity entity) {
return new UserResponse(
entity.getId(),
entity.getName(),
entity.getEmail()
);
}
}
Spring Boot에서 DTO 자동 변환
Spring Boot에서 DTO 변환을 쉽게 하기 위해 ModelMapper 또는 MapStruct와 같은 라이브러리를 사용할 수 있습니다.
import org.modelmapper.ModelMapper;
@Service
public class UserService {
private final ModelMapper modelMapper = new ModelMapper();
public UserDTO convertToDTO(UserEntity entity) {
return modelMapper.map(entity, UserDTO.class);
}
}
DTO의 장점
데이터 보호:
데이터베이스와 직접 연결된 Entity를 노출하지 않아 민감한 데이터 보호.
역할 분리:
Entity와 DTO를 분리하여 비즈니스 로직과 데이터 전송 책임을 명확히 구분.
API 표준화:
클라이언트 요청과 응답 형식을 명확히 정의 가능.
유지보수성 향상:
데이터 전송 형식 변경이 Entity와 독립적으로 이루어짐.
결론
DTO(Data Transfer Object)는 계층 간 데이터를 주고받을 때 사용되는 단순한 객체입니다.
데이터 캡슐화, 보안성 강화, API 설계 표준화 등의 장점으로, 특히 Spring MVC와 RESTful API 설계에서 널리 사용됩니다.
DTO를 활용하여 애플리케이션의 구조를 더 유연하고 유지보수 가능하게 설계할 수 있습니다.
@RequestMapping("/prefix")
@RestController
public class RequestMappingController {
// Post, GET, Put, Patch, Delete 모두 가능
@GetMapping(value = "/v3")
public String exampleV3() {
// logic
return "this is sparta!";
}
}
@PathVariable
📌 URL 경로에 포함된 변수를 메서드의 매개변수로 전달하기 위해 사용됩니다. 주로RESTful 웹 서비스에서 동적인 요청 처리를 위해 사용됩니다.
HTTP 특성 중 하나인 비연결성을 극복하여 데이터를 전달하기 위한 방법 중 하나이다. URL로 전달된 값을 파라미터로 받아오는 역할을 수행한다.
동작 원리
URL 경로에 **변수(placeholder)**를 정의하고, 해당 값을 컨트롤러 메서드의 매개변수로 전달합니다.
@PathVariable은 요청 경로에서 변수 값을 추출하여 메서드 파라미터에 바인딩합니다.
@PathVariable
경로 변수를 중괄호에 둘러싸인 값으로 사용할 수 있다.
ex) user/{id}
기본적으로 @PathVariable로 설정된 경로 변수는 반드시 값을 가져야 하며 값이 없으면 응답 상태코드 404 Not Found Error가 발생한다.
최근 Restful API를 설계하는 것이 API의 기준이 되며 해당 어노테이션의 사용 빈도가 높아졌다.
@RequestMapping("/posts")
@RestController
public class PathVariableController {
// postId로 된 post 단건 조회
@GetMapping("/{postId}")
public String pathVariableV1(@PathVariable("postId") Long data) {
// logic
String result = "PathvariableV1 결과입니다 : " + data;
return result;
}
}
@RequestMapping("/posts")
@RestController
public class PathVariableController {
// 변수명과 같다면 속성값 생략가능
@GetMapping("/{postId}")
public String pathVariableV2(@PathVariable Long postId) {
// logic
String result = "PathvariableV2 결과입니다 : " + postId;
return result;
}
}
2. @PathVariable 다중 사용 가능
@RestController
public class PathVariableController {
@GetMapping("/{postId}/comments/{commentId}")
public String pathVariableV3(
@PathVariable Long postId,
@PathVariable Long commentId
) {
// logic
String result = "PathvariableV3 결과입니다 postId : " + postId + "commentsId : " + commentId;
return result;
}
}
@RequestMapping("/posts/{postId}")
@RestController
public class PathVariableController {
@GetMapping("/comments/{commentId}")
public String pathVariableV4(
@PathVariable Long postId,
@PathVariable Long commentId
) {
// logic
String result = "PathvariableV4 결과입니다 postId : " + postId + "commentsId : " + commentId;
return result;
}
}
특정 파라미터 매핑
📌 속성 설정을 통하여 특정 헤더, 특정 파라미터와 Mapping 할 수 있다.
Parameter 추가 매핑
특정 파라미터와 매핑하는 방법
package com.example.springbasicannotation.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ParameterController {
// parms 속성값 추가
@GetMapping(value = "/users", params = "gender=man")
public String params() {
// logic
String result = "params API가 호출 되었습니다.";
return result;
}
}
실제 URL GET http://localhost:8080/users**?gender=man** 파라미터가 있어야 호출된다.
실행결과
파라미터가 없다면?
400 Bad Request 클라이언트 측 에러
속성 작성 규칙
params = "gender"
params의 key값은 커스텀이 가능하다
value는 없어도 된다.
params = "!gender"
gender가 없어야 한다.
params = "gender=man"
gender=man 이어야 한다.
params = "gender!=man"
params의 value값이 man가 아니여야 한다.
params = {"gender=man", "gender=woman"}
배열로 속성 값을 여러 개 설정이 가능하다.
특정 Header 매핑
특정 Header와 매핑하는 방법
@RestController
public class ParameterController {
// headers 속성값 추가
@PostMapping(value = "/users", headers = "Content-Type=application/json")
public String headers() {
// logic
String result = "headers API가 호출 되었습니다.";
return result;
}
}
Postman → Body → raw → JSON
Postman → Headers → hidden
HTTP Header를 사용하기 때문에 Postman으로 테스트 해야 한다.
ex) key=Content-Type / value=application/json
실행결과
속성 작성 규칙은 위 params 속성 값의 규칙과 같다.
MediaType 매핑, consume(수용)
HTTP Header Content-Type(요청)과 매핑 된다.
@RestController
public class ParameterController {
// consumes 속성값 추가
@PostMapping(value = "/users", consumes = "application/json") // MediaType.APPLICATION_JSON_VALUE
public String consumes() {
// logic
String result = "consumes API가 호출 되었습니다.";
return result;
}
}
consumes 속성 value값으로는 이미 Spring에서 제공되는 Enum인
MediaType.APPLICATION_JSON_VALUE 형태로 사용한다.
Postman → Body → raw → JSON
Postman → Body → raw → JSON
파라미터가 없거나 다르다면?
HTTP 상태코드 405 Unsupported Media Type Exception 발생
속성 작성 방법
consumes=”application/json”
application/json 미디어 타입 허용
consumes=”!application/json”
application/json 제외 미디어 타입 허용
consumes=”application/*”
application/ 으로 시작하는 모든 미디어 타입 허용
consumes=”*\\/*”
모두 허용
MediaType 매핑 produces(제공)
요청 헤더의 Accept 값에 따라서 produces 하는 값이 변한다.
@RestController
public class ParameterController {
// produces 속성값 추가
@GetMapping(value = "/users", produces = "text/plain")
public String produces() {
// logic
String result = "text/plain 데이터 응답";
return result;
}
}
Map과 유사하게 Key, Value 형식으로 구현되어 있지만 하나의 Key가 여러 Value를 가질 수 있다 HTTP Header, Reqeust Parameter와 같이 하나의 Key에 여러 값을 받을 때 사용한다.
ex ) key1=value1&key1=value2
MultiValueMap<String, String> linkedMultiValuemap = new LinkedMultiValueMap();
// key1에 value1 저장
linkedMultiValuemap.add("key1", "value1");
// key1에 value2 저장
linkedMultiValuemap.add("key1", "value2");
// key1에 저장된 모든 value get
List<String> values = linkedMultiValuemap.get("key1");
서버는 고객이 요청한 **주문 정보(예: 치킨 배달)**를 확인하고, 이 요청을 처리할 적절한 음식점(Handler)을 찾아야 합니다.
여기서Handler Mapping은주문에 맞는 음식점을 조회하는 과정입니다.
예: 고객이 "치킨"을 주문했으니, 근처의 치킨집을 선택합니다.
3. Handler를 처리할 Adapter 조회
음식점마다 주문을 처리하는 방식(프로세스)이 다를 수 있습니다.
어떤 음식점은 앱을 통해 바로 확인하고,
어떤 음식점은 전화로 주문을 확인해야 합니다.
이 단계에서는 요청을 처리할 수 있는 적절한 **Handler Adapter(음식점과 서버를 연결하는 도구)**를 찾습니다.
예: 치킨집은 앱으로 주문을 확인하는 시스템이 있으니,앱 기반 주문 어댑터를 선택.
4. Handler Adapter 실행(handle)
적절한 **Handler Adapter(주문 어댑터)**가 음식점에 주문을 전달합니다.
예: 앱이 "치킨 한 마리 주문 요청"을 음식점의 주문 시스템에 보냅니다.
5. Handler 실행(호출)
음식점(Handler)이 실제로 주문을 준비하기 시작합니다.
예: 주방에서 치킨을 튀기고 포장합니다.
6. Model And View 반환(return)
음식점은 준비가 끝난 뒤, "주문이 완료되었습니다!"라는 메시지와 함께 준비된 치킨 데이터를 서버에 보냅니다.
예: "치킨 1마리, 포장 완료!"
여기서ModelAndView는 이 데이터("치킨 데이터")와 응답 메시지를 함께 묶어서 반환하는 역할을 합니다.
7. View Resolver 호출 (알맞은 View 요청)
서버는 고객에게 응답을 보내기 전에, 결과 데이터를 어떻게 보여줄지 결정합니다.
예: 고객이 앱을 통해 결과를 볼 수 있어야 하므로, 앱 화면(View)을 설정해야 합니다.
View Resolver는 "치킨 준비 완료"라는 정보를 앱의 알맞은 화면(예: 주문 완료 페이지)으로 연결합니다.
8. View 반환
View Resolver는 "주문 완료 화면"을 앱에 전달합니다.
예: 고객의 앱에 "주문 완료! 배달 중!"이라는 화면이 나타남.
9. View Rendering
고객의 앱에서 실제로 화면이 표시됩니다.
예: 고객은 "주문이 완료되었고, 곧 배달됩니다!"라는 메시지를 화면에서 확인합니다.
결론: 배달 서비스로 비유한 실행 흐름
고객이 배달 앱에 주문 요청을 보냄 (HTTP 요청)
서버가 요청을 처리할 음식점을 찾음 (Handler Mapping)
주문을 전달하는 적절한 방식을 선택 (Handler Adapter 조회)
음식점이 주문을 준비함 (Handler 실행)
음식점이 준비 상태를 서버에 반환 (ModelAndView 반환)
서버가 결과를 앱 화면에 맞게 변환 (View Resolver 호출)
결과가 앱 화면에 표시됨 (View Rendering)
요약
DispatcherServlet ( 요청과 응답의 중앙 처리국 )
클라이언트 HTTP Request를 알맞게 파싱하고 클라이언트에게 알맞은 응답을 반환
핸들러 목록 정보를 알고있다.
핸들러 어댑터 목록 정보를 알고있다.
HandlerAdapter (부서 담당 연결원)
자신이 처리할 수 있는 Handler인지 확인할 수 있는 기능(Method)이 필요하다.
프론트 컨트롤러에서 요청을 위임받았을 때 핸들러에게 요청을 지시하는 기능이 필요하다.
return 시 Handler로부터 전달받은 결과를 알맞은 응답으로 변환한다.
Handler (처리자)
요청에 대한 로직을 수행하는 기능이 필요하다.
Dispatcher Servlet
📌 모든 HTTP 요청의 진입점으로 작동하는 프론트 컨트롤러(Front Controller)로, 요청 처리의 중앙 허브(총괄), 핸들러 매핑(배정), 핸들러 어댑터 실행(인력 파견), 응답 반환(우편전달)을 다 한다.
Spring MVC의 프론트 컨트롤러는Dispatcher Servlet(Servlet의 한 종류)이다.
Dispatcher Servlet은 HttpServlet을 상속 받아서 사용하고 Servlet의 한 종류이다.
Spring Boot는 Dispatcher Servlet을 서블릿으로 자동으로 등록(내장 Tomcat WAS를 실행하면서 등록한다)하고 모든 URL 경로에 대해서 Dispatcher Servlet을 Mapping 한다. → (urlPatterns=”/”) = 요청이 들어오는 모든 url에 총괄 관리자를 붙인다.
더 자세한 URL 경로가 높은 우선순위를 가진다. = 요구사항이 자세한 손님부터 처리해 주겠다는 것 = 효율적이라서
개발자가 만들 Servlet이 항상 우선순위가 높아서 실행된다.
DispatcherServlet의 service()
Servlet이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
Spring MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 Override 해두었다.
FrameworkServlet.service()를 시작으로 여러 메서드가 호출됨과 동시에 가장 중요한DispatcherServlet.doDispatch()가 호출된다.
📌 Spring MVC는 DispatcherServlet 코드의 변경 없이 기능변경 및 확장이 가능하다. 기능들이 대부분 Interface로 만들어져 있기 때문이다.
이쯤에서 한번 더 짚고 넘어가면 왜 대부분이 클래스가 아니라 인터페이스로 구현되어 있냐는 질문엔, 다형을 위해서가 정답이다.
org.springframework.web.servlet
HandlerMapping
HandlerAdapter
ViewResolver
View
당연히 이 모든 인터페이스를 알 필요는 없다. 하지만 자주 사용하는 인터페이스에 대해서는 잘 알아야 확장이나 구현, 문제 해결에 있어 용이하다.
Controller Interface
📌Spring 2.5 이전에 모든 컨트롤러가 구현해야 했던 표준 인터페이스입니다. 이 인터페이스는 클라이언트의 요청을 처리하고, 결과를 View로 전달하는 기본적인 방법을 정의합니다.
Controller Interface를 implements 하여 구현하게되면 개발자가 원하는 Controller(Handler)를 사용할 수 있게됩니다.
추후 강의에 등장할 현대에 사용하는 Annotation 기반 Spring의 @Controller와는 역할이 비슷하지만 연관은 없습니다.
package com.example.springbasicmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
// Spring Bean 이름을 URL로 설정
@Component("/example-controller")
public class ExampleController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("example-controller가 호출 되었습니다.");
return null;
}
}
http://localhost:8080/example-controller 로 HTTP 요청을 하게 되면 응답결과가 반환된다.
출력 결과
@Component
Spring Bean에 등록하는 역할을 수행한다.
Spring Bean은 애플리케이션의 구성 요소를 정의하는 객체이다.
마치 Servlet이 Servlet Container에 등록되는 것과 같다.
Handler Mapping
핸들러 매핑에서 ExampleController( Spring 애플리케이션에서 특정 요청을 처리하는 컨트롤러 클래스)를 찾을 수 있어야 한다.
→ Spring Bean의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
Handler Adapter
Handler Mapping을 통해 찾은 핸들러를 실행할 수 있는 Handler Adapter가 필요
→ Controller Interface를 실행할 수 있는 Handler Adapter를 찾고 실행한다.
놀랍게도 Handler Mapping은 찾기만 하고 Handler Adapter가 Handler에게 배치해줘야 한다.
유지보수와 확장성에 용이하다...
Spring Boot의 Handler Mapping, Handler Adapter
📌 Spring Boot를 사용하면 개발에 필요하여 자동으로 등록되는 HandlerMapping과 HandlerAdapter들이 있다.
HandlerMapping, HandlerAdapter 모두 우선순위대로 조회한다.
HandlerMapping
우선순위 순서
RequestMappingHandlerMapping
우선순위가 가장 높다
Annotation 기반 Controller의 @RequestMapping에 사용
BeanNameUrlHandlerMapping(위 예시코드에 사용)
Spring Bean Name으로 HandlerMapping
HandlerAdapter
우선순위 순서
RequestMappingHandlerAdapter
Annotation 기반 Controller의 @RequestMapping에서 사용
HttpRequestHandlerAdapter
HttpRequestHandler 처리
SimpleControllerHandlerAdapter(위 예시코드에 사용)
Controller Interface 처리
@RequestMapping 은 가장 높은 우선순위의 HandlerMapping인 RequestMappingHandlerMapping 과 가장 높은 우선순위의 HandlerAdapter인 RequestMappingHandlerAdapter 두가지를 사용하며 현대에 사용하는 Annotation 기반의 컨트롤러를 지원한다.
HttpRequestHandler로 알아보는 Spring MVC 동작 순서
// 인터페이스
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
// 구현체
@Component("/request-handler")
public class ExampleRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("request-handler Controller 호출");
// 구현 로직
}
}
@Component("/error-controller")
public class WhitelabelErrorController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("error-controller가 호출 되었습니다.");
// viewName "sparta"는 존재하지 않는다.
return new ModelAndView("sparta");
}
}
컨트롤러는 호출되지만 View를 못 찾아 Whitelabel Error Page를 응답한다.
Spring Boot의 ViewResolver
📌 Spring Boot를 사용하면 개발에 필요하여 자동으로 등록되는 ViewResolver들이 있다.
우선순위 순서
아래 두 가지 이외에도 많은 ViewResolver가 존재한다.
BeanNameViewResolver
Bean Name으로 View를 찾아 반환
InternalResourceViewResolver(위 예시코드)
application.properties 설정 파일에 등록한 prefix, suffix 설정 정보를 사용하여 ViewResolver 등록
// 아래 코드를 자동으로 해주는것과 마찬가지이다.
@Bean
InternalResourceViewResolver internalResourceViewResolver() {
return new InternalResourceViewResolver("/WEB-INF/views", ".jsp");
}
InternalResourceViewResolver로 알아보는 Spring MVC 동작 순서
📌 Java Application Framework로 엔터프라이즈 애플리케이션 개발에 주로 사용된다.
엔터프라이즈 애플리케이션은 대규모로 복잡한 비즈니스 프로세스와 데이터를 처리하는 애플리케이션을 뜻한다.
IoC (Inversion of Control), AOP (Aspect-Oriented Programming), DI (Dependency Injection) 등의 개념을 바탕으로 개발되었으며, 이를 통해 애플리케이션의 구조를 유연하고 모듈화하여 유지보수성을 높이고, 복잡한 엔터프라이즈 애플리케이션 개발을 더 간단하게 만들어 줍니다.
Spring Framework로 만드는 Web Application 라면 : Java 냄비 : Spring
[1] Spring Framework 특징
애플리케이션의 다양한 구성 요소를 유연하게 연결하고 관리할 수 있도록 해준다.
Spring Framework는 누구나 사용할 수 있는 오픈소스 이다.
모듈화되어 있어 필요에 따라 특정 기능만 선택적으로 사용할 수 있다.
Java언어의 가장 큰 특징인 객체 지향 언어의 특징을 살려낸 프레임워크이다.
캡슐화
상속
추상화
다형성
[2] Spring의 구조
Spring Framework는 여러 모듈로 구성되어 있으며, 각 모듈은 특정 목적에 맞는 기능을 제공합니다. 주요 모듈은 다음과 같습니다:
Core Container:
Core: Spring의 핵심 기능인 IoC (Inversion of Control) 및 DI (Dependency Injection)를 제공.
Beans: 객체 생성 및 관리.
Context: Spring의 애플리케이션 컨텍스트 기능을 제공, 이벤트 관리와 같은 다양한 기능 포함.
Spring AOP: AOP 기능을 제공하여, 메서드 호출 전후의 동작을 정의.
Data Access/Integration:
JDBC: 데이터베이스와의 상호작용을 쉽게 할 수 있도록 지원.
ORM: Hibernate, JPA 등 객체-관계 매핑을 처리.
JMS: Java 메시지 서비스 (JMS)를 지원하여 메시지 기반 애플리케이션 구현.
Web:
Spring MVC: 웹 애플리케이션을 위한 모델-뷰-컨트롤러 패턴을 지원.
WebSocket: 실시간 통신을 위한 WebSocket 지원.
WebFlux: 반응형 프로그래밍 모델을 기반으로 한 웹 프레임워크.
Security:
인증, 권한 부여 및 다양한 보안 관련 기능을 제공합니다.
Testing:
Spring Test 모듈은 Spring 애플리케이션을 테스트하는 데 필요한 다양한 유틸리티를 제공합니다. @SpringBootTest 등을 통해 통합 테스트를 쉽게 할 수 있습니다.
[3] Spring Framework의 주요 특징
1. IoC (Inversion of Control) / DI (Dependency Injection)
Spring은 객체를 직접 생성하는 대신, IoC 컨테이너를 통해 객체를 관리합니다. 객체 간의 의존 관계를 DI 방식으로 해결하여, 애플리케이션의 결합도를 낮추고 유연성을 높입니다.
예를 들어, 하나의 클래스가 다른 클래스에 의존할 때, 이를 자동으로 주입해주는 방식입니다.
@Component
public class Service {
private final Repository repository;
@Autowired // Repository를 자동으로 주입
public Service(Repository repository) {
this.repository = repository;
}
}
2. AOP (Aspect-Oriented Programming)
AOP는 관점 지향 프로그래밍으로, 핵심 비즈니스 로직과 공통 기능(로깅, 트랜잭션 관리 등)을 분리하여 개발할 수 있게 도와줍니다. 이로 인해 코드가 더 깔끔해지고 재사용성이 증가합니다.
Spring에서는 AOP를 이용해 메서드 호출 전후에 특정 처리를 추가할 수 있습니다.
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Method called: " + joinPoint.getSignature().getName());
}
}
3. 모듈화
Spring은 여러 개의 모듈로 구성되어 있어 필요한 기능만 선택적으로 사용할 수 있습니다. 예를 들어, Spring Data, Spring Security, Spring Web 등 다양한 서브모듈을 통해 애플리케이션의 요구에 맞는 기능을 추가할 수 있습니다.
4. 트랜잭션 관리
Spring은 선언적 트랜잭션 관리 기능을 제공하여, 비즈니스 로직에서 트랜잭션을 쉽게 관리할 수 있도록 도와줍니다. @Transactional 어노테이션을 사용해 메서드 또는 클래스 수준에서 트랜잭션을 자동으로 관리할 수 있습니다.
@Transactional
public void transferMoney(Account fromAccount, Account toAccount, double amount) {
// 돈 이체 처리 로직
}
5. Spring MVC
Spring MVC는 웹 애플리케이션을 위한 모델-뷰-컨트롤러(MVC) 프레임워크입니다. 요청을 처리하고, 뷰를 렌더링하며, 사용자 인터페이스를 구축하는 데 필요한 기능을 제공합니다.
Spring MVC는 RESTful API 개발도 지원하여, REST 기반의 웹 서비스를 쉽게 구축할 수 있습니다.
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}
6. Spring Boot
Spring Boot는 Spring Framework의 복잡성을 줄여주는 자동 설정과 내장 서버를 제공합니다. 이를 통해 설정을 최소화하고, 독립 실행형 애플리케이션을 쉽게 만들 수 있습니다. Spring Boot는 Spring Cloud와 결합하여 마이크로서비스 아키텍처를 쉽게 구현할 수 있습니다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
7. Spring Security
Spring Security는 인증(Authentication)과 권한 부여(Authorization)를 처리하는 프레임워크로, 웹 애플리케이션에 필요한 보안 기능을 제공합니다. 로그인, 로그아웃, CSRF 방어, 권한 체크 등의 기능을 손쉽게 설정할 수 있습니다.
8. Spring Data
Spring Data는 데이터베이스와의 상호작용을 단순화하는 데 도움을 줍니다. 특히 JPA(Java Persistence API)를 사용하여 관계형 데이터베이스와의 통합을 돕고, CRUD 작업을 자동화합니다.
📌 코드에 메타데이터를 추가할 수 있는 기능을 제공하며 주로 코드에 특별한 의미를 부여하거나, 컴파일러와 런타임에 특정 동작을 트리거하기 위해 사용된다.
어노테이션은 코드에서 직접적인 로직 실행에 영향을 미치지 않지만, 코드의 의미를 설명하거나 추가적인 처리를 위해 사용됩니다.
"명함"를 생각하면 편하다. 이 명함은 "사람"에서 "프로그래머인 사람"이 된다. 사람이라는 정체성은 그대로이지만, 이 사람의 용도를 알 수 있다. 코드의 용도를 표시하며 실제로 컴파일러도 그 의미를 알지만 프로그램(사람) 자체에는 변화가 없다.
[1] 어노테이션 정의
어노테이션은 @ 기호로 시작하며, 클래스, 메서드, 변수, 매개변수, 패키지 등에 추가할 수 있다.
[2] 내장 어노테이션
@Override
메서드가 상위 클래스나 인터페이스의 메서드를 오버라이드하고 있음을 나타낸다.
이때 컴파일러는 메서드가 실제로 오버라이드하고 있는지 확인한다.
@Deprecated
해당 요소가 더 이상 사용되지 않음을 나타낸다.
해당 어노테이션이 붙은 코드를 사용하면 컴파일 경고가 발생한다.
@SuppressWarnings
컴파일러 경고를 억제한다.
사용되지 않는 변수에 대한 경고를 무시할 수 있다.
[3] 사용자 정의 어노테이션
개발자가 필요에 따라 직접 어노테이션을 정의할 수 있다.
사용자 정의 어노테이션은 특정 메타데이터를 추가하거나,
AOP(Aspect-Oriented Programming) 같은 기술과 결합하여 다양한 기능을 구현할 수 있다.
AOP는 심화 주차에 배울 내용
Lombok
📌 Java에서 반복적인 코드를 줄여주는 라이브러리로, 코드의 가독성과 유지보수성을 높이는 데 도움이 됩니다. 주로 getter, setter, toString, equals, hashCode 메서드와 같은 보일러플레이트 코드를 자동으로 생성해줍니다.
[1] Lombok 사용 시 장점:
보일러플레이트 코드 감소: Lombok은 반복적인 코드(예: getter, setter, 생성자 등)를 자동으로 생성해 주어 코드가 간결해집니다.
코드의 가독성 향상: 중요한 로직에 집중할 수 있어 코드가 더 깔끔하고 이해하기 쉬워집니다.
생산성 향상: 반복적인 코드 작성에 소모되는 시간을 절약할 수 있습니다.
[2] Lombok 사용 시단점:
자동 생성된 코드의 가시성 부족: Lombok은 컴파일 시 코드 생성이 이루어지기 때문에, IDE에서 코드가 어떻게 처리되는지 확인하기 어려운 경우가 있습니다.
디버깅 어려움: Lombok이 생성한 코드는 실제로 파일에 존재하지 않기 때문에, 디버깅 시 자동으로 생성된 메서드를 추적하는 데 어려움이 있을 수 있습니다.
@Getter, @Setter
클래스의 모든 필드에 대한 getter와 setter 메서드를 자동으로 생성한다.
예시 코드
@Getter
@Setter
public class User {
private String name;
private int age;
/** 아래 코드를 @Getter, @Setter 어노테이션이 생성해준다.
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
**/
}
위 코드에서 getName(), setName(String name), getAge(), setAge(int age) 메서드가 자동으로 생성된다.
@ToString
객체의 toString() 메서드를 자동으로 생성한다.
기본적으로 클래스의 모든 필드를 포함하며, 특정 필드를 제외하거나 포맷을 지정할 수도 있다.
예시코드
@ToString
public class User {
private String name;
private int age;
}
toString() 메서드는 객체를 String으로 변환해주는 역할을 수행한다.
@EqualsAndHashCode
equals()와 hashCode() 메서드를 자동으로 생성한다.
객체의 동일성과 해시 코드를 정의하는데 사용된다.
예시 코드
@EqualsAndHashCode
public class User {
private String name;
private int age;
}
📌 웹 애플리케이션에서 클라이언트(브라우저)가 웹 페이지를 렌더링하는 방식입니다. 서버는 최소한의 HTML을 클라이언트에 전달하고, 이후 JavaScript가 클라이언트 측에서 실행되어 콘텐츠를 동적으로 렌더링하는 방식입니다.
HTML을 요청한다. 비어있는 HTML을 응답받는다. JS가 존재하는 주소 링크를 응답한다.
자바스크립트(클라이언트 로직, 렌더링 포함)를 요청한다.
HTTP API 요청을 하고 화면에 필요한 데이터를 JSON 형태(JSON이 아니어도됨)로 응답받는다.
응답받은 JSON 데이터로 HTML을 동적으로 그린다.
[1] CSR의 작동 원리
클라이언트 요청:
사용자가 웹 브라우저에서 특정 URL을 요청하면, 서버는 최소한의 HTML 파일을 클라이언트에 전달합니다. 이 HTML은 보통 페이지의 뼈대(구조)만 포함하고 있으며, 실제 콘텐츠는 포함되지 않습니다.
JavaScript 로드:
클라이언트가 받은 HTML은 주로 JavaScript 파일을 불러오는 역할을 합니다. JavaScript는 페이지를 동적으로 렌더링하는 데 필요한 데이터와 로직을 처리합니다.
API 호출:
JavaScript는 서버와 API 요청을 통해 데이터를 받아옵니다. 이 데이터는 JSON 형식으로 전달되며, JavaScript는 이 데이터를 바탕으로 페이지를 동적으로 생성합니다.
동적 렌더링:
받은 데이터를 바탕으로 JavaScript가 HTML을 동적으로 생성하여 화면에 표시합니다. 클라이언트에서 페이지 렌더링이 이루어지므로, 서버는 추가적인 렌더링 작업을 수행하지 않습니다.
인터랙티브한 페이지:
JavaScript는 페이지에서 사용자의 상호작용을 처리하며, 페이지 내에서 동적 콘텐츠 갱신이나 애니메이션 등을 실시간으로 구현합니다.
[2] CSR의 장점
서버 부하 감소:
클라이언트 측에서 렌더링을 처리하므로 서버 부하가 감소합니다. 서버는 페이지의 최소한의 HTML만 보내면 되며, 클라이언트에서 나머지 작업을 처리합니다.
빠른 사용자 경험:
초기 페이지 로딩 후, 페이지 내의 다른 콘텐츠를 동적으로 로드할 수 있습니다. 이는 사용자가 페이지 간 이동을 할 때 빠른 응답 시간을 제공합니다.
인터랙티브한 경험:
CSR은 JavaScript를 사용하여 동적이고 인터랙티브한 사용자 경험을 제공합니다. 버튼 클릭, 폼 제출 등의 인터랙션에 즉각적으로 반응할 수 있습니다.
클라이언트 측 상태 관리:
CSR에서는 클라이언트 측에서 애플리케이션의 상태를 관리할 수 있어, 페이지를 새로고침하지 않고도 실시간으로 UI 업데이트가 가능합니다.
애플리케이션 로딩 후 빠른 탐색:
한 번 페이지가 로드되면, 후속 페이지들은 빠르게 로드되고 전환됩니다. 이는 **SPA(Single Page Application)**에서 더욱 두드러지며, 페이지를 새로고침하지 않고도 새로운 콘텐츠를 동적으로 로드합니다.
[3] CSR의 단점
초기 로딩 속도:
CSR은 클라이언트에서 페이지를 렌더링하므로, 초기 페이지 로딩 시에 JavaScript 파일과 데이터를 모두 로드해야 합니다. 이로 인해 초기 로딩 속도가 상대적으로 느릴 수 있습니다.
SEO (검색 엔진 최적화):
CSR에서는 JavaScript로 동적으로 콘텐츠를 렌더링하므로, 검색 엔진 크롤러가 자바스크립트로 렌더링된 콘텐츠를 제대로 인식하지 못할 수 있습니다. 이는 SEO에 불리할 수 있습니다. 최근에는 **서버 사이드 렌더링(SSR)**과 결합하거나 프리렌더링을 통해 이 문제를 해결하기도 합니다.
자바스크립트 의존성:
CSR 방식은 JavaScript가 제대로 실행되지 않으면 페이지가 제대로 표시되지 않거나, 애플리케이션이 기능을 제대로 수행하지 않을 수 있습니다. 사용자가 JavaScript를 비활성화한 경우 페이지가 정상적으로 동작하지 않을 수 있습니다.
클라이언트 성능 문제:
클라이언트(브라우저)가 많은 작업을 처리하므로, 저사양 기기에서는 성능 저하가 발생할 수 있습니다. 특히, 복잡한 애플리케이션에서는 클라이언트가 느려질 수 있습니다.
적절한 패턴을 가지고 작성 되었지만 HTTP의 메소드 별로 서비스를 구분하여 사용하고 있지는 않다.
즉, 서비스 형태나 작업의 종류에 맞추어 적절한 HTTP 메소드를 지정하고 있지 않다.
사용자의 요청을 GET, POST로 대부분 처리하고 에러를 반환한다.
요청 예시(리소스에 대해 분리된 엔드포인트를 가진다)
POST /users
{
"name": "sparta",
"password": "codingclub"
}
Level2
우리가 제공하고자 하는 리소스를 적절하게 용도와 상태에 따라서 HTTP Methods에 맞게 설계하고 서비스하는 단계.
만약 리소스의 상태가 읽기 용도로 사용되는 데이터라고 한다면 GET Method를 사용한다.
새로운 리소스를 추가하는 경우는 POST Method
기존 리소스의 상태를 변경하기 위해서는 PUT, PATCH Method
리소스를 삭제하고자 할 때에는 Delete Method를 사용하여 서비스의 상태를 표현한다.
RESTful Service의 DB에 저장된 리소스를 확인하고 이러한 데이터를 조작하기 위해서 CRUD와 매칭되는 HTTP Methods를 이용하여 서비스 하는 것을 Level2 단계라고 한다.
HTTP의 메소드를 이용하여 리소스의 상태를 구분하여 서비스 하게 되면 비슷한 이름의 URI라 하더라도 HTTP Method에 따라서 다른 형태의 서비스를 제공할 수 있게 된다.
요청 예시(HTTP Method 활용)
GET /users/123 // 특정 사용자 조회
POST /users // 사용자 생성
{
"name": "sparta",
"password": "codingclub"
}
PUT /users/123 // 사용자 정보 수정
{
"name": "java",
"password": "spring"
}
DELETE /users/123 // 사용자 삭제
Level3
데이터를 가지고 그 다음 작업에서 어떠한 작업을 할 수 있는지 상태 정보를 함께 넘겨준다.
클라이언트 측에서는 서버가 제공하는 서비스를 일일이 찾는 수고를 겪지 않아도 된다.
엔드포인트만 가지고 있으면 서버가 제공할 수 있는 다음, 그 다음 URI값을 알 수 있다.
요청 예시(응답 내에 링크를 포함한다)
💡HATEOAS(Hypermedia As The Engine Of Application State)회원 가입 후 회원 정보 수정은 어떻게 해야 하는지, 조회는 어떻게 해야 하는지 회원 조회를 하면서 그다음 단계로 진행할 수 있는 또 다른 리소스에 대한 정보는 어떠한 것이 있는지. 이러한 모든 정보를 같이 알려주는 기능을 HATEOAS라고 한다.