Java에서는 null 값에 대해서 접근 하려고 할 때 null pointer exception이 발생 함으로, 이러한 부분을 방지 하지 위해서 미리 검증을 하는 과정

  1. 검증해야 할 값이 많은 경우 코드의 길이가 길어 진다
  2. 구현에 따라서 달라 질 수 있지만 Service Logic과의 분리가 필요
  3. 흩어져 있는 경우 어디에서 검증을 하는지 알기 어려우며, 재사용의 한계가 있음
  4. 구현에 따라 달라 질 수 있지만, 검증 Logic이 변경 되는 경우 테스트 코드 등 참조하는 클래스에서 Logic이 변경되어야 하는 부분이 발생 할 수 있음

 

어노테이션 의미 기타
@Size 문자 길이 측정 Int Type 불가
@NotNull null 불가  
@NotEmpty null, “” 불가  
@NotBlank null, “”,” “불가  
@Past 과거 날짜  
@PastOrPresent 오늘이거나 과거 날짜  
@Future 미래 날짜  
@FutureOrPresent 오늘이거나 미래 날짜  
@Pattern 정규식 적용  
@Max 최대값  
@Min 최소값  
@AssertTrue / False 별도 Logic 적용  
@Valid 해당 object valication 실행  

 

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation' // 추가
}

Gradle 디펜던시에 validation 추가

 

 

 

package com.example.validation.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public class User {

    @NotBlank
    private String name;

    @Min(value = 0)
    private int age;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phoneNumber;

    /* 이하 생략  */ 
}

@Valid 을 사용한 객체는 안에 Validation과 관련된 어노테이션이 붙은 멤버 변수(속성) 값을 검증한다.

각 어노테이션은 message 값 설정을 통해 에러 발생 시 원하는 메세지로 수정 가능하다.

 

package com.example.validation.controller;

import com.example.validation.dto.User;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ApiController {

    @PostMapping("/user")
    public ResponseEntity user(@Valid @RequestBody User user, BindingResult bindingResult) {

        if(bindingResult.hasErrors()) {
            StringBuilder sb = new StringBuilder();
            bindingResult.getAllErrors().forEach(objectError -> {
                FieldError field = (FieldError) objectError;
                String message = field.getDefaultMessage();

                System.out.println("field : " + field.getField() + ", message : " + message);

                sb.append("field : ").append(field.getField()).append(", message : ").append(message);
            });

            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(sb.toString());
        }

        // logic

        return ResponseEntity.ok(user);
    }

}

컨트롤러 단에서 검증 시에는 BindingResult 객체를 활용하여 에러 발생 및 메시지 확인이 가능하다.

 

 Spring Boot Custom Validation

 

1. AssertTrue / False 와 같은 method 지정을 통해서 Custom Logic 적용 가능
    메소드명을 is*로 시작해야 한다.

 

package com.example.validation.dto;

import jakarta.validation.constraints.*;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class User {

    @NotBlank
    private String name;

    @Min(value = 0)
    private int age;

    @Email
    private String email;

    @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 양식과 맞지 않습니다. 01x-xxx(x)-xxxx")
    private String phoneNumber;

    @Size(min = 6, max = 6)
    private String reqYearMonth; // yyyyMM


     /* 중간 생략 */

    public String getReqYearMonth() {
        return reqYearMonth;
    }

    public void setReqYearMonth(String reqYearMonth) {
        this.reqYearMonth = reqYearMonth;
    }

    @AssertTrue(message = "yyyyMM의 형식에 맞지 않습니다.")
    public boolean isReqYearMonthValidation() {

        try {
            LocalDate localDate = LocalDate.parse(getReqYearMonth()+"01", DateTimeFormatter.ofPattern("yyyyMMdd"));
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                ", reqYearMonth='" + reqYearMonth + '\'' +
                '}';
    }
}

 

2. ConstraintValidator 를 적용하여 재사용이 가능한 Custom Logic 적용 가능
    - message: 유효성 검사 실패 시 알릴 메시지 설정
    - groups: 유효성 검사를 상황별로 분리하여 적용할 때 사용.
    -payload: 검증 실패 시 추가 정보를 전달하거나 메타데이터를 활용할 때 사용.
    -pattern (커스텀):  YearMonthValidator에서 검증 시 사용할 패턴 설정

 

package com.example.validation.annotation;

import com.example.validation.validator.YearMonthValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Constraint(validatedBy = {YearMonthValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface YearMonth {
    String message() default "yyyyMM 형식에 맞지 않습니다.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String pattern() default "yyyyMMdd";

}

@Constraint 어떤 Custom Validator 클래스로 검증을 할 것 인지 설정한다.  Custom Validator 클래스는 ConstraintValidator 인터페이스를 구현한 클래스로 정의할 수 있다.
ConstraintValidator<어노테이션명, 타입명>

 

package com.example.validation.validator;

import com.example.validation.annotation.YearMonth;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class YearMonthValidator implements ConstraintValidator<YearMonth, String> {

    private  String pattern;

    @Override
    public void initialize(YearMonth constraintAnnotation) {
        this.pattern = constraintAnnotation.pattern();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {

        try {
            LocalDate localDate = LocalDate.parse(value+"01", DateTimeFormatter.ofPattern(this.pattern));
        } catch (Exception e) {
            return false;
        }
        return true;
    }
}

initialize에서 커스텀 어노테이션 사용 시 설정한 값을 가져온다. 그 후에 isValid 메소드에서 검증 로직을 추가한다.

 

 

package com.example.validation.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;

import java.util.List;

public class User {

    @NotBlank
    private String name;

    @Max(value = 90)
    private int age;

    @Valid
    private List<Car> cars;


	  /* 생략 */
}

Validation를 적용한 객체 안에 또 다른 객체가 있을 경우에 또 다른 객체 또한 Validator이 필요한 경우에는 반드시 @Valid 어노테이션을 추가해야한다.

'Spring' 카테고리의 다른 글

Filter  (1) 2025.04.15
Spring Boot Exception 처리  (0) 2025.03.24
Java Bean 규약  (0) 2025.03.24
AOP 관점지향 프로그램  (0) 2025.03.23
IoC와 DI  (0) 2025.03.19
규약 항목  설명
기본 생성자 매개변수가 없는 기본 생성자를 가져야 합니다.
멤버 변수 접근 제어자 멤버 변수는 private으로 선언되어야 합니다.
Getter/Setter 메서드 멤버 변수에 대한 getter와 setter 메서드를 제공해야 하며, 이는 public으로 선언되어야 합니다.

속성 값을 가져올 때는 getProperty() 또는 isProperty() 형식의 Getter를 사용. 속성 값을 설정할 때는 setProperty(value) 형식의 Setter를 사용.
직렬화 선택적으로 Serializable 인터페이스를 구현하여 직렬화할 수 있습니다.

 

'Spring' 카테고리의 다른 글

Spring Boot Exception 처리  (0) 2025.03.24
Validation  (0) 2025.03.24
AOP 관점지향 프로그램  (0) 2025.03.23
IoC와 DI  (0) 2025.03.19
ObjectMapper  (0) 2025.03.18

AOP (Aspect Oriented Programming) - 관점지향 프로그램

횡단 관심사를 모듈화하여 여러 객체나 계층에 걸쳐 적용할 수 있는 프로그래밍 방식이다.

코드의 중복을 줄이고, 유지 보수성을 높일 수 있다.

- Web Layer : REST API를 제공, Client 중심의 로직 적용
- Business Layer : 비즈니스 로직 처리, 트랜잭션 관리
- Data Layer: 데이터베이스와의 통신, 데이터 저장 및 조회.

 

어노테이션  설명
@Aspect 해당 클래스가 Aspect임을 명시합니다.
@Pointcut Advice가 적용될 메서드의 범위를 지정합니다.
@Before 대상 메서드 실행 전에 Advice를 실행합니다.
@AfterReturning 대상 메서드가 정상적으로 실행된 후에 Advice를 실행합니다.
@AfterThrowing 대상 메서드에서 예외가 발생했을 때 Advice를 실행합니다.
@After 대상 메서드 실행 후에 Advice를 실행합니다.
@Around 대상 메서드 실행 전, 후 또는 예외 발생 시에 Advice를 실행합니다.

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop' // 추가
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

Gradle에서 aop dependency를 추가한다.

 

package com.example.aop.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect // AOP 적용
@Component // 컴포넌트로써 스프링에서 관리를 위해 빈으로 등록
public class ParameterAop {

    //aop.controller 패키지안의 메소드 전체
    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}

    @Before("cut()")
    public void before(JoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println(method.getName());

        Object[] args = joinPoint.getArgs();
        for(Object obj : args){
            System.out.println("type : " + obj.getClass().getSimpleName() );
            System.out.println("value : " + obj );
        }
    }

		//returning 응답으로 반환되는 객체
    @AfterReturning(value = "cut()", returning = "returnObj" )
    public void afterReturn(JoinPoint joinPoint,Object returnObj){
        System.out.println("return obj");
        System.out.println(returnObj);
    }

}

// JoinPoint 들어가는 지점에 대한 정보들을 가지고 있는 객체

1. @Pointcut 메소드를 생성하여 AOP 적용할 범위 설정
2. @Before 설정을 통해 Pointcut 메소드가 실행 되기 전에 들어온 값(인자) 확인
3. @AfterReturning설정을 통해 Pointcut 메소드가 정상 실행 후 반환되는 값(Object) 확인

이와 같은 방법으로 로깅과 같은 기능을 서비스 로직과 코드를 분리할 수 있다.

 

package com.example.aop.annotaion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD}) // 이 어노테이션을 클래스, 인터페이스 등에 적용, 메소드에 적용
@Retention(RetentionPolicy.RUNTIME) // 런타임 시에 어노테이션 정보를 조회할 수 있음
public @interface Timer {
}

@Target : 어노테이션을 적용할 요소를 정의 한다. 
@Retention : 어노테이션의 유지 정책을 설정

위와 같이 커스텀 어노테이션 정의를 할 수있다.

 

예제 1 : 실행시간 측정

package com.example.aop.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

// @Bean 은 클래스에 사용할수 없고, 메소드에 가능하다.
// @Component 는 클래스 단위로 빈을 등록 @Configuration 은 하나의 클래스에 여러 빈을 등록 할수 있다.
@Aspect
@Component
public class TimerAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}


    @Pointcut("@annotation(com.example.aop.annotaion.Timer)")
    private void enableTimer(){}


    @Around("cut() && enableTimer()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 메소드 실행 후 return 값이 있으면 반환
        Object result = joinPoint.proceed();

        stopWatch.stop();

        System.out.println("total time: " + stopWatch.getTotalTimeSeconds());
    }

}

&& 연산자를 이용하여 Pointcut 조건을 여러 개 설정 할 수 있다. @Around를 활용하여 aop 설정이 적용한 메소드에 한하여 joinPoint.proceed(실제 메소드 실행) 코드 전과 후에 일괄적으로 특정한 처리를 추가 할 수 있다. 위의 예시는 해당하는 메소드의 실행 시간을 측정한 것이다.

 

예제 2 : 디코딩

package com.example.aop.annotaion;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {
}

 

package com.example.aop.aop;

import com.example.aop.dto.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Aspect
@Component
public class DecodeAop {

    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut(){}


    @Pointcut("@annotation(com.example.aop.annotaion.Decode)")
    private void enableDecode(){}


    @Before("cut() && enableDecode()")
    public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
        Object[] args = joinPoint.getArgs();

        for(Object arg : args){
            if(arg instanceof User){
                User user = User.class.cast(arg);
                String base64Email = user.getEmail();
                String email = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
                user.setEmail(email);
            }

        }
    }

    @AfterReturning(value = "cut() && enableDecode()", returning = "returnObj")
    public void afterReturning(JoinPoint joinPoint, Object returnObj){
        if(returnObj instanceof User){
            User user = User.class.cast(returnObj);
            String email = user.getEmail();
            String base64Email = new String(Base64.getEncoder().encode(email.getBytes(StandardCharsets.UTF_8)));
            user.setEmail(base64Email);
        }
    }

}

클라이언트 측에서 암호화 된 요청을 서버에서 복호화 할 때 인터셉터나 필터 단계에서는 변환하려고 하면 톰캣 자체에서 Body를 한번 읽으면 더 이상 읽을 수 없도록 막아놨기 때문에 작업이 어렵다.

하지만 AOP 같은 구간에서는 이미 필터와 인터셉터를 지나서 값 자체가 객체화 되었기 때문에 그 값을 변환 해주거나 특정한 객체를 넣어 줄 수도 있다.

'Spring' 카테고리의 다른 글

Validation  (0) 2025.03.24
Java Bean 규약  (0) 2025.03.24
IoC와 DI  (0) 2025.03.19
ObjectMapper  (0) 2025.03.18
Response 내려주기  (0) 2025.03.18

IoC (Inversion of Control) - 제어의 역전

스프링에서 일반적인 JAVA 객체를 new로 생성하여 개발자가 직접 관리 하는 것이 아닌 Spring Container에게 모두 맡김으로 객체 관리 권한이 개발자에서 →프레임워크 넘어 갔음을 “제어의 역전”이라고 한다.

 

 IoC의 핵심 개념

- 의존성 역전: 객체가 다른 객체를 생성하거나 관리하는 대신, 외부에서 객체를 주입받아 사용합니다.
- 의존성 주입: 객체가 필요로 하는 의존성을 외부에서 제공받는 방식으로, 이는 생성자, setter 메서드 등을 통해 이루어집니다.
- 컨테이너: 객체의 생성 및 관리를 담당하는 역할로, Spring Framework에서는 ApplicationContext가 이러한 역할을 수행합니다.

 

IoC의 장점

 

- 결합도 감소: 객체 간의 결합도가 낮아져 코드의 유연성과 재사용성이 높아집니다.
- 테스트 용이성: 의존성을 쉽게 교체할 수 있어 테스트가 용이합니다.
- 유지보수성 향상: 객체 간의 의존 관계가 명확해져 유지보수가 쉬워집니다.

 

// 의존성 역전 이전
public class Car {
    private Engine engine;
    
    public Car() {
        engine = new GasolineEngine(); // 직접 생성
    }
}

// 의존성 역전 이후 (의존성 주입)
public class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine; // 외부에서 주입
    }
}

 

 

DI(Dependency Injection) - 의존성 주입

  • 의존성으로 부터 격리시켜 코드 테스트에 용이
  • DI를 통하여, 불가능한 상황을 Mock와 같은 기술을 통하여, 안정적으로 테스트 가능하다.
  • 코드를 확장하거나 변경 할 때 영향을 최소화 한다
  • 순환참조를 막을 수 있다
public interface IEncoder {
    String encode(String message);
}



import java.util.Base64;

public class Base64Encoder implements IEncoder {

    @Override
    public String encode(String message)
    {
        return Base64.getEncoder().encodeToString(message.getBytes());
    }
}



import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class UrlEncoder implements IEncoder{

    @Override
    public String encode(String message){
        try {
            return URLEncoder.encode(message,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
}



public class Encoder {
    private IEncoder iEncoder;

		// 외부에서 의존하는 객체 주입 -> 의존성 주입(DI)
    public Encoder(IEncoder iEncoder) {
        this.iEncoder = iEncoder;
    }


    public String encode(String message)
    {
        return iEncoder.encode(message);
    }

}

public class Main {
    public static void main(String[] args) {

        String url = "www.naver.com/books/it?page=10&size=20&name=spring-boot";


        Encoder encoder = new Encoder(new Base64Encoder());
        String result = encoder.encode(url);
        System.out.println(result);

    }
}

 

@Component

객체의 생명 주기를 개발자가 아닌 스프링 컨테이너에 할당하기 위해 @Component 어노테이션을 사용하여 빈을 정의한다.

빈을 정의하면 스프링 프레임워크에서 의존성을 주입(DI) 해준다. DI 방법은 여러가지가 있다.

  1. 생성자 주입
@Service
public class MyAccountService implements AccountService {
    private final RiskAssessor riskAssessor;
    
    @Autowired
    public MyAccountService(RiskAssessor riskAssessor) {
        this.riskAssessor = riskAssessor;
    }
}

 

    2. Setter 주입

@Service
public class MyAccountService implements AccountService {
    private RiskAssessor riskAssessor;
    
    @Autowired
    public void setRiskAssessor(RiskAssessor riskAssessor) {
        this.riskAssessor = riskAssessor;
    }
}

 

    3. 필드 주입

@Service
public class MyAccountService implements AccountService {
    @Autowired
    private RiskAssessor riskAssessor;
}

 

 

package com.example.springioc;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return context;
    }
}

정의된 빈들은 ApplicationContext에서 찾을 수 있다.

 

package com.example.springioc;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class Encoder {
    private IEncoder iEncoder;

    public Encoder(@Qualifier("base64Encoder") IEncoder iEncoder) {
        this.iEncoder = iEncoder;
    }

    public void setIEncoder(IEncoder iEncoder) {
        this.iEncoder = iEncoder;
    }

    public String encode(String message)
    {
        return iEncoder.encode(message);
    }

}

생성자를 통해 의존성을 주입을 받을 때 동일한 타입의 빈들이 여러 개 있을 경우 @Qualifier 어노테이션을 통해 어떤 빈을 주입 받을지 선택할 수 있다.

 

 

package com.example.springioc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@SpringBootApplication
public class SpringIocApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringIocApplication.class, args);
        ApplicationContext context = ApplicationContextProvider.getApplicationContext();

        Base64Encoder base64Encoder = context.getBean(Base64Encoder.class);
        UrlEncoder urlEncoder = context.getBean(UrlEncoder.class);

				//@Qualifier("base64Encoder")으로 base64Encoder 빈 주입
        Encoder encoder = context.getBean(Encoder.class);

				// Encoder 클래스로 정의된 빈 중에 이름으로 빈을 찾아 주입 
        Encoder encoder2 = context.getBean("urlEncode", Encoder.class);

    }
}


@Configuration
class AppConfig{

    @Bean("base64Encode")
    public Encoder encoder1(Base64Encoder base64Encoder) {
        return new Encoder(base64Encoder);
    }

    @Bean("urlEncode")
    public Encoder encoder2(UrlEncoder urlEncoder) {
        return new Encoder(urlEncoder);
    }

}

 

'Spring' 카테고리의 다른 글

Java Bean 규약  (0) 2025.03.24
AOP 관점지향 프로그램  (0) 2025.03.23
ObjectMapper  (0) 2025.03.18
Response 내려주기  (0) 2025.03.18
REST API 시작하기  (0) 2025.03.18
package com.example.obejctmapper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ObjectMapperApplicationTests {

    @Test
    void contextLoads() throws JsonProcessingException {
        System.out.println("--------------");

        // Text JSON -> Object
        // Object -> Text JSON

        // controller req json(text) -> object
        // response object -> json(text)


        var objectMapper = new ObjectMapper();

        // object -> text
        // object mapper get 메소드를 활용
        var user = new User("jun",20,"010-1111-2222");
        var text = objectMapper.writeValueAsString(user);
        System.out.println(text);

        // text -> object
        // object mapper 는 디폴트 생성자를 필요로 한다.
        var objectUser = objectMapper.readValue(text, User.class);
        System.out.println(objectUser);
    }

}

objectMapper는 object 타입을 text로 변환할 때 get메소드를 활용한다. 멤버 변수의 get메소드 이외에 get-으로 시작하는 메소드가 있다면 변환 과정에서 에러가 발생할 수 있으니 주의하자.

 

text를 object로 변환 할때는 디폴트 생성자가 반드시 있어야 동작한다.

 

 

package com.example;

import com.example.dto.Car;
import com.example.dto.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();

        User user = new User();
        user.setName("홍길동");
        user.setAge(10);


        Car car1 = new Car();
        car1.setName("K5");
        car1.setCarNumber("11가 1111");
        car1.setType("sedan");

        Car car2 = new Car();
        car2.setName("Q5");
        car2.setCarNumber("22가 2222");
        car2.setType("SUV");


        List<Car> carList = Arrays.asList(car1, car2);
        user.setCars(carList);

        System.out.println(user);

        String json = objectMapper.writeValueAsString(user);
        System.out.println(json);


        JsonNode jsonNode = objectMapper.readTree(json);
        String _name = jsonNode.get("name").asText();
        int _age = jsonNode.get("age").asInt();
        System.out.println("name : " + _name);
        System.out.println("age : " + _age);


        JsonNode cars = jsonNode.get("cars");
        ArrayNode arrayNode = (ArrayNode) cars;
        List<Car> _cars = objectMapper.convertValue(arrayNode, new TypeReference<List<Car>>() {});
        System.out.println(_cars);

        ObjectNode objectNode = (ObjectNode) jsonNode;
        objectNode.put("name","jun");
        objectNode.put("age",20);

        System.out.println(objectNode.toPrettyString());


    }
}

ObjectMapper의 readTree 메소드를 통해 문자열 형태의 JSON 데이터를 JsonNode로 변경할 수 있다. JsonNode에서 get 메소드를 통해 값을 추출할 수 있지만, Array 타입과 같은 경우에는 새로운 노드로 취급하므로 바로 사용이 불가하다. 따라서 다시 JsonNode로 추출하여 형변환 한 후 convertValue로 원하는 클래스 타입으로 변경한다.

JsonNode는 내부적으로 값을 변경할 수 없지만 ObjectNode로 형변환 후 put 메소드를 활용하면 값을 업데이트 하는 방식으로 수정이 가능하다.

 

이러한 방식을 잘 활용하기 위하면 JSON 표준 스펙을 잘 정의하고 사전에 미리 인지하고 있어야 한다.

'Spring' 카테고리의 다른 글

AOP 관점지향 프로그램  (0) 2025.03.23
IoC와 DI  (0) 2025.03.19
Response 내려주기  (0) 2025.03.18
REST API 시작하기  (0) 2025.03.18
Decorator pattern(데코레이터 패턴)  (0) 2021.04.18
package com.example.response.controller;

import com.example.response.dto.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class ApiController {

    // TEXT
    @GetMapping("/text")
    public String text(@RequestParam String account) {
        return account;
    }

    // JSON
    // req -> object mapper -> object -> method -> object -> object mapper -> json -> response
    @PostMapping("/json")
    public User json(@RequestBody User user) {
        return user;
    }


    // ResponseEntity
    @PutMapping("/put")
    public ResponseEntity<User> put(@RequestBody User user){
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }


}

1. text
    plain/text가 응답으로 반환한다.
    
2. JSON
    object mapper가 요청을 object로 변환 후 DTO클래스에 매칭시킨다. 
    객체를 응답으로 직접 내려줄 경우 object mapper가 해당 클래스를 JSON형태로 반환한다.
    
3. ResponseEntity
    헤더, 응답 코드 등등 응답에 대한 커스터마이징이 필요한 경우 반환타입으로 ResponseEntity를 활용한다.

 

package com.example.response.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User {
    private String name;
    private Integer age;
    private String phoneNumber;
    private String address;


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", phoneNumber='" + phoneNumber + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

 

뷰(페이지)를 반환하는 @Controller 어노테이션

 

@Controller는 뷰를 반환하는 반면, @RestController는 JSON이나 XML과 같은 형식으로 데이터를 직접 반환합니다.

@RestController는 @Controller와 @ResponseBody를 결합한 어노테이션으로, 모든 메서드가 응답 본문으로 데이터를 반환하도록 설정합니다. 주로 RESTful API에서 사용됩니다

@Controller 선언 후 응답 결과를 String 값을 반환하면 해당 파일을 리소스 폴더에서 찾아서 반환한다.

 

package com.example.response.controller;

import com.example.response.dto.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class PageController {

    @RequestMapping("/main")
    public String main(){
        return "main.html";
    }

//   아래의 경우는 뷰(페이지)가 아니므로 일반적으로 사용 되지 않음  
//    //ResponseEntity
//    @GetMapping("/user/{userId}")
//    public ResponseEntity<User> user(@PathVariable String userId){
//        var user = new User();
//        user.setName("Jack");
//        user.setAddress("Seoul");
//        return ResponseEntity.status(HttpStatus.CREATED).body(user);
//    }
//
//    @GetMapping("/user")
//    @ResponseBody
//    public User user2(){
//        var user = new User();
//        user.setName("Jack");
//        user.setAddress("Seoul");
//        return user;
//    }

}

 

'Spring' 카테고리의 다른 글

IoC와 DI  (0) 2025.03.19
ObjectMapper  (0) 2025.03.18
REST API 시작하기  (0) 2025.03.18
Decorator pattern(데코레이터 패턴)  (0) 2021.04.18
Proxy pattern(프록시 패턴)  (0) 2021.04.16

시작하기

원하는 프로젝트 명으로 설정 및 JAVA, Gradle,, JDK, Jar 파일을 설정한다.

 

의존성 설정에서 Spring 웹 어플리케이션 개발을 위한 Spring Web을 선택

 

controller 패키지를 생성하고 controller 역할을 수행할 클래스를 생성한다.
class에 @RestController, @RquestMapping 어노테이션을 추가한다.
    @RestController: 해당 클래스를 REST API를 처리하는 컨트롤러로 설정
    @RequestMapping : 클라이언트 요청을 받을 URI를 지정
    ex) http://localhost:8080/api
추가로 해당 컨트롤러에서 GET방식으로 클라이언트 요청을 처리할 메소드를 생성
   @GetMapping : 해당 메소드는 GET 처리 및 URI 지정 
   ex) GET http://localhost:8080/api/hello251

 

GET API

package com.example.hello.controller;

import com.example.hello.dto.UserRequest;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api/get")
public class GetApiController {

    @GetMapping(path = "/hello") // http://localhost:port/api/get/hello
    public String hello() {

        return "GET HELLO";
    }

    @RequestMapping(path = "/hi", method = RequestMethod.GET)  // GET http://localhost:port/api/get/hi
    public String hi(){
        return "hi";
    }

    // http://localhost:port/api/get/path-variable/{name}
    @GetMapping("/path-variable/{name}")
    public String pathVariable(@PathVariable(name="name") String pathName){
        System.out.println("PathVariable : "+pathName);
        return pathName;
    }

    // http://localhost:port/api/get/query-param?user=jun&email=jun@gmail.com&age=21
    @GetMapping(path = "/query-param")
    public String queryParam(@RequestParam Map<String,String> queryParams)
    {
        StringBuilder sb = new StringBuilder();
        queryParams.entrySet().forEach( entry -> {
            System.out.println(entry.getKey() + "=" + entry.getValue()+"\n");

            sb.append(entry.getKey() + "=" + entry.getValue()+"\n");
        });
        return sb.toString();
    }

    @GetMapping(path = "/query-param2")
    public String queryParam2(
            @RequestParam String name,
            @RequestParam String email,
            @RequestParam int age
    ){
        System.out.println(name + " " + age + " " + email);

        return name + " " + age + " " + email;
    }
    @GetMapping(path = "/query-param3")
    public String queryParam3(UserRequest userRequest){
        System.out.println(userRequest.getName() + " " + userRequest.getAge() + " " + userRequest.getEmail());

        return userRequest.toString();
    }

}

controller로 사용할 클래스을 생성한다. 이 후 @RestController, @RequestMapping 어노테이션을 추가

1. @GetMapping 
    메소드에 @GetMapping를 추가하면 해당 메소드는 GET 방식으로 요청을 받는다.
    인자의 기본값은 path이며, path = “stringValue” 사용 시 명시적으로 설정 가능
    
2. @RequestMapping
    메소드에 @RequestMapping를 추가 시 모든 방식의 메소드로 요청을 받을 수 있음
    필요에 따라 인자로 method = RequestMethod.GET 와 같이 사용할 **HTTP Method**를 직접 지정할 수 있다.
    
3. PathVariable
    URI에서 변화 하는 값은 PathVariable로 지정하여 한 URI로 처리가 가능하다. 맵핑 어노테이션에서@GetMapping("/path-variable/{name}") 와 같이 **({)대괄호**로 감싸주면 PathVariable로 지정이 된다. 
    
    일반적으로 URI의 지정한 PathVariable와 메소드 안에서 사용할 변수명이 동일하게끔 사용하지만, 필요에 따라 @PathVariable(name="name") String pathName 와 같이 어노테이션 name 속성에 PathVariable을 지정하면 다른 변수명으로 사용이 가능하다.
    
4. QueryParam
    URI중 ? 뒤에 key=value의 쌍을 말한다. 여러 개 일 경우 &를 구분자로 사용한다.
    
    1. @RequestParam와 Map<String,String>을 이용하여 임의의 쿼리 파라미터를 받을 수 있다.
    2. @RequestParam를 각각 지정하여 지정한 쿼리 파라미터만을 받을 수 있다.
    3. DTO 클래스를 만들어 파싱할 쿼리 파라미터 관리할 수 있다. 이 때는 @RequestParam를 사용하지 않는다.
        
        DTO(Data Transfer Object)는 프로세스 간 데이터를 전달하는 객체를 의미합니다. Spring에서는 주로 Controller와 Client 간의 데이터 교환을 위해 사용됩니다.
        
        DTO의 특징:
        - 순수하게 데이터를 저장하고 전달하는 목적으로만 사용
        - 비즈니스 로직을 포함하지 않고 getter/setter 메서드만 포함
        - 계층간 데이터 전달을 위한 객체로써 데이터 은닉과 캡슐화를 위해 사용
        
        위 예제에서 UserRequest 클래스는 클라이언트로부터 받은 name, email, age 데이터를 담아 전달하는 DTO 역할을 수행합니다.

 

package com.example.hello.dto;

public class UserRequest {
    private String name;
    private String email;
    private int age;


    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    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;
    }

    @Override
    public String toString() {
        return "UserRequest{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }
}

 

POST API

 


JSON
JSON(JavaScript Object Notation)은 데이터를 쉽게 교환하고 저장하기 위한 텍스트 기반의 데이터 교환 표준입니다. 이는 자바스크립트의 객체 표기법에서 파생된 부분 집합으로, 인간이 읽고 쓰기 쉽고 기계가 분석하고 생성하기도 쉬운 경량 데이터 교환 형식입니다

JSON 구조
JSON 데이터는 **키-값 쌍**으로 구성되며, 중괄호 **`{}`**로 둘러싸여 표현됩니다. 여러 데이터는 쉼표 **`,`**로 구분됩니다

JSON 데이터 타입
JSON에서 사용할 수 있는 기본 데이터 타입은 다음과 같습니다:

- 숫자(number): 정수 또는 부동소수점 숫자로, 큰 따옴표 없이 사용됩니다.
- 문자열(string): 큰 따옴표로 묶여 사용됩니다.
- 불리언(boolean): true 또는 false로 표현됩니다.
- 객체(object): 중괄호 `{}`로 묶여 사용됩니다.
- 배열(array): 대괄호 `[]`로 묶여 사용됩니다.
- NULL: null로 표현됩니다

 

JSON 객체 예시

{
  "name": "John",
  "age": 30,
  "city": "New York"
}

 

ObjectMapper

ObjectMapper는 Jackson 라이브러리의 핵심 클래스로, Java 객체와 JSON 간의 변환을 담당합니다. 이 클래스는 직렬화와 역직렬화 두 가지 주요 기능을 제공합니다.

1. 직렬화 (Serialization)
- Java 객체를 JSON으로 변환: `writeValueAsString()` 메서드를 사용하여 Java 객체를 JSON 문자열로 변환합니다. 이 과정은 객체의 필드를 JSON의 키-값 쌍으로 매핑합니다

2. 역직렬화 (Deserialization)
- JSON을 Java 객체로 변환: `readValue()` 메서드를 사용하여 JSON 문자열을 Java 객체로 변환합니다. 이 과정은 JSON의 키-값 쌍을 Java 객체의 필드로 매핑합니다. 기본 생성자를 통해 객체를 생성한 후, 리플렉션을 사용하여 필드에 값을 할당합니다
    
    ObjectMapper를 사용하여 Java 객체를 JSON으로 변환할 때, 기본적으로는 camelCase로 변환됩니다.
    

동작 과정
1. JSON 파싱: JSON 데이터를 파싱하여 키-값 쌍을 추출합니다.
2. 객체 생성: 기본 생성자를 통해 빈 Java 객체를 생성합니다.
3. 리플렉션 사용: 리플렉션을 통해 객체의 필드를 탐색하고, JSON의 키와 일치하는 필드에 값을 할당합니다
4. 결과 반환: 완성된 Java 객체를 반환합니다.

 

package com.example.post.controller;

import com.example.post.dto.PostRequestDto;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class PostApiController {

    @PostMapping("/post")
    public void post(@RequestBody Map<String, Object> requestData) {
        requestData.forEach((key, value) ->
                System.out.println("key: " + key + " value: " + value));
    }

    @PostMapping("/post2")
    public void post(@RequestBody PostRequestDto requestDto) {
        System.out.println("requestDto: " + requestDto);

    }


}

 

 

// 예제
{
  "account" : "user1",
  "email" : "jun@gmail.com",
  "address" : "경기도 OO시",
  "password" : "abcd",
  "phone_number" : "010-1111-2222"
}

 

기본적으로  RequestBody를 파싱하기 위하여 @RequestBody 어노테이션을 추가해야한다.

1. Map<String, Object>
    메소드에 @PostMapping를 추가하면 해당 메소드는 POST 방식으로 요청을 받는다.
    JSON타입의 RequestBody를 파싱하기 위해 Map<String, Object>를 사용할 수 있다.
    
2. DTO 클래스
    데이터 클래스를 만들어 RequestBody를 파싱할 데이터를 지정할 수 있다.
    JSON 형태의 RequestBody를 ObjectMapper가 DTO 객체로 변환 할 때 JAVA 클래스 변수명은 카멜 케이스로 JSON은 스네이크 케이스로 되어 있어 제대로 동작하지  경우에는 @JsonProperty 어노테이션을 사용하여 변수와 매칭될 JSON Key값을 직접 지정할 수 있다.

 

 

package com.example.post.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

public class PostRequestDto {
    private String account;
    private String email;
    private String address;
    private String password;
    
    @JsonProperty("phone_number")
    private String phoneNumber;

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    @Override
    public String toString() {
        return "PostRequestDto{" +
                "account='" + account + '\'' +
                ", email='" + email + '\'' +
                ", address='" + address + '\'' +
                ", password='" + password + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                '}';
    }
}

 

PUT API

POST API와 방식이 유사하다.

 

package com.example.put.controller;

import com.example.put.dto.PutRequestDto;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class PutApiController {

    @PutMapping("/put")
    public PutRequestDto put(@RequestBody PutRequestDto requestDto)
    {
        System.out.println(requestDto);

        return requestDto;
    }
}

 

package com.example.put.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class CarDto {

    private String name;
    private String carNumber;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCarNumber() {
        return carNumber;
    }

    public void setCarNumber(String carNumber) {
        this.carNumber = carNumber;
    }

    @Override
    public String toString() {
        return "CarDto{" +
                "name='" + name + '\'' +
                ", carNumber='" + carNumber + '\'' +
                '}';
    }
}

 

package com.example.put.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import java.util.List;

@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class PutRequestDto {

    private String name;
    private int age;
    private List<CarDto> carList;


    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;
    }

    public List<CarDto> getCarList() {
        return carList;
    }

    public void setCarList(List<CarDto> carList) {
        this.carList = carList;
    }


    @Override
    public String toString() {
        return "PostRequestDto{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", carList=" + carList +
                '}';
    }
}

 

{
  "name":"jun",
  "age" : 20,
  "car_list": [
      {
        "name" : "BMW",
        "car_number": "11가 1234"
      },
      {
        "name" : "A4",
        "car_number": "22가 3456"
      }
    ]
}

 

DELETE API

삭제와 관련된 API 이며, 키 값이 제한적이기 때문에 보통 DTO 클래스를 따로 생성하진 않는다.
이미 삭제가 되어 있더라도 200 OK를 응답으로 내려준다.

package com.example.delete.controller;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class DeleteApiController {

    @DeleteMapping("/delete/{userId}")
    public void delete(@PathVariable String userId, @RequestParam String account) {
        System.out.println(userId);
        System.out.println(account);
    }
}

'Spring' 카테고리의 다른 글

ObjectMapper  (0) 2025.03.18
Response 내려주기  (0) 2025.03.18
Decorator pattern(데코레이터 패턴)  (0) 2021.04.18
Proxy pattern(프록시 패턴)  (0) 2021.04.16
Adapter pattern(어댑터 패턴)  (0) 2021.04.16

1. 문제 설명

한국중학교에 다니는 학생들은 각자 정수 번호를 갖고 있습니다. 이 학교 학생 3명의 정수 번호를 더했을 때 0이 되면 3명의 학생은 삼총사라고 합니다. 예를 들어, 5명의 학생이 있고, 각각의 정수 번호가 순서대로 -2, 3, 0, 2, -5일 때, 첫 번째, 세 번째, 네 번째 학생의 정수 번호를 더하면 0이므로 세 학생은 삼총사입니다. 또한, 두 번째, 네 번째, 다섯 번째 학생의 정수 번호를 더해도 0이므로 세 학생도 삼총사입니다. 따라서 이 경우 한국중학교에서는 두 가지 방법으로 삼총사를 만들 수 있습니다.

한국중학교 학생들의 번호를 나타내는 정수 배열 number가 매개변수로 주어질 때, 학생들 중 삼총사를 만들 수 있는 방법의 수를 return 하도록 solution 함수를 완성하세요.

 

- 제한 사항

  • 3 ≤ number의 길이 ≤ 13
  • -1,000 ≤ number의 각 원소 ≤ 1,000
  • 서로 다른 학생의 정수 번호가 같을 수 있습니다.

2. 풀이 코드

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> number) {
    int answer = 0;
    int n = number.size();
    
    
    for(int i=0;i<n-2;i++)
    {
        for(int j=i+1;j<n-1;j++)
        {
            for(int k=j+1;k<n;k++)
            {
                if(number[i]+number[j]+number[k] == 0)
                    answer++;
            }
            
        }
    }
    
    return answer;
    
}

 

3. 정리

배열에서 3개를 뽑아 0이 되는 모든 경우를 찾아야 하기 때문에 삼중 루프를 이용하여 모든 경우를 탐색한다.

 

전체 시간 복잡도는 삼중루프 이기 때문에 O(n³) 이다.

배열의 길이가 짧기 때문에 시간 초과는 나지 않는다.

 

출처 : https://school.programmers.co.kr/learn/courses/30/lessons/131705

+ Recent posts