728x90
반응형

1. Redis docker image pull

$ docker pull redis # redis 이미지 받기
$ docker images # redis 이미지 확인
$ docker run -p 6379:6379 --name some-redis -d redis # redis 시작하기
$ docker ps # redis 실행 확인

 

 

2. 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

 

3. application.properties

#Redis
spring.redis.host= localhost
spring.redis.port= 6379

 

 

Redis Cache 활성화를 위한 @Annotation

  • @EnableCaching
    • SpringBoot에게 캐싱 기능이 필요하다고 전달
    • SpringBoot Starter class에 적용
  • @Cacheable
    • DB에서 애플리케이션으로 데이터를 가져오고 Cache에 저장하는 데 사용
    • DB에서 데이터를 가져오는 메서드에 적용
  • @CachePut
    • DB의 데이터 업데이트가 있을 때 Redis Cache에 데이터를 업데이트
    • DB에서 PUT/PATCH와 같은 업데이트에서 사용
  • @CacheEvict
    • DB의 데이터 삭제가 있을 때 Redis Cache에 데이터를 삭제
    • DB에서 DELETE와 같은 삭제에서 사용

 

4. RedisConfig 파일 생성

/backend/redis/RedisConfig

package web.backend.redis;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public CacheManager testCacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofMinutes(3L));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
    }
}

매니저에 도메인을 Serialize 하게끔 설정해두었기에 도메인에 따로 Serializable를 import 안해줘도 된다.

다만 어노테이션에서 매니저를 호출해야 한다.

 

backend/WebConfig

@Import({AopConfig.class, RedisConfig.class})

 

5. LocalDateTime in Domain

Serialize를 위해 LocalDateTime 타입 필드는 설정을 따로 해줘야 한다.

backend/module/user/User

...

    @CreatedDate
    @Column(name="user_createdat", updatable = false)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime user_creadtedat;

    @LastModifiedDate
    @Column(name="user_updatedat")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime user_updatedat;
    
...

 

6. Service 적용

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

    @Cacheable(value = "User", cacheManager = "testCacheManager")
    public List<User> findAll() {
        return userSpringJPARepository.findAll();
    }

    @Cacheable(value = "User", key = "#id", cacheManager = "testCacheManager")
    public User findByIndexId(Long id) {
        return userSpringJPARepository.findById(id).get();
    }

    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }

    @CachePut(value = "Order", key = "#id", cacheManager = "testCacheManager")
    public String update(Long id, User user) {
        User userOne = userSpringJPARepository.findById(id).get();
        userOne.changeUser(user.getUserId());
        return "ok";
    }

    @CacheEvict(value = "Order", key = "#id", cacheManager = "testCacheManager")
    public String delete(Long id) {
        userSpringJPARepository.deleteById(id);
        return "ok";
    }


}

 


테스트

1. 데이터 추가

테스트를 위해 WebConfig에 어플리케이션 빌드 시 데이터를 추가하는 로직을 작성해준다.

package web.backend;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.aop.AopConfig;
import web.backend.interceptor.LogInterceptor;
import web.backend.module.user.User;
import web.backend.module.user.repository.UserSpringJpaRepository;
import web.backend.redis.RedisConfig;

@Configuration
@Import({AopConfig.class, RedisConfig.class})
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }

    @Autowired
    UserSpringJpaRepository userSpringJpaRepository;

    @Bean
    public void addUsers() {
        for(int i = 0; i < 100; i++) {
            User user = new User();
            user.changeUser("member" + i);
            userSpringJpaRepository.save(user);
        }
    }

}

 

 

2. Redis Docker Container

Spring Server를 키고 Redis Container에 접속한 후 요청을 보내서 로그를 확인해보자.

$ docker ps    
$ docker exec -it some-redis /bin/bash
$ redis-cli monitor

 

postman으로 요청을 보내보자.

그러면 아래와 같이 캐시가 저장된 것을 확인할 수 있다.

 

postman에도 데이터가 잘 도착했다.

이번엔  key를 지정하고 다시 보내보자. ( id를 파라미터로 보내는 로직 )

 

역시 정상적으로 동작이 잘 되었다.

 

 

3. 성능 확인

 

- 처음 캐시가 사용되기 전 요청

 

- 두번 째 요청

 

 


다만,

아래와 같은 점을 고려해야 한다.

  1. 리스트를 처음에 캐싱한다.
  2. 리스트의 어떤 항목이 생성되거나 수정되거나 삭제된다.
  3. 캐시된 리스트의 데이터가 바뀌어야 하지만 그렇지 못한다.

캐시에 변경사항이 생길 경우 다른 캐시에 영향이 끼친다면 로직을 따로 만들어 주어야 한다. 당장에라도 적용하고 싶으면

findAll()의 @Cacheable 어노테이션은 제거한 상태로 쓰고 나중에 각 테이블의 연관관계를 고려해서 로직을 구성해주자.

728x90
반응형
728x90
반응형

외부 api에 접속할 때 간혹 가다 한 번이 아니라 두세 번 정도 시도를 해야 성공하는 경우가 있다. ( 외부 사이트가 불안정할 경우 )

 

그럴 때 만들 수 있는 Aspect가 있다. default 값은 꼭 정해주도록 하자.

 

1. Retry Annotation

backend/annotation/Retry

package web.backend.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 3;
}

 

2. RetryAspect

backend/aop/RetryAspect

package web.backend.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import web.backend.annotation.Retry;

@Slf4j
@Aspect
public class RetryAspect {

    @Around("@annotation(retry)")
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
        log.info("[retry] {} retry={} ", joinPoint.getSignature(), retry);

        int maxRetry = retry.value();
        Exception exceptionHolder = null;

        for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
            try {
                log.info("[retry] try count={}/{}", retryCount, maxRetry);
                return joinPoint.proceed();
            } catch (Exception e) {
                exceptionHolder = e;
            }
        }
        throw exceptionHolder;
    }
}

 

3. AopConfig

backend/aop/AopConfig

...

    @Bean
    public RetryAspect retryAspect() { return new RetryAspect(); }

...

 


테스트

Service 부분을 잠시 바꿔보고 해보자.

backend/module/user/UserService

...

    private static int seq = 0;

    @Retry(value = 2)
    public String save(User user) {
        if (seq == 0) {
            seq++;
            throw new IllegalStateException("예외 발생!!");
        }
        return "ok";
    }
    
...

 

실행 결과

728x90
반응형
728x90
반응형

전 포스트에서 만든 공통 응답객체를 활용해서 모든 컨트롤러에서 발생하는 에러를 핸들링해보자.

 

1. CustomRuntimeException

backend/exception/CustomRuntimeException

package web.backend.exception;

public class CustomRuntimeException extends RuntimeException {
    public CustomRuntimeException() {
    }

    public CustomRuntimeException(String message) {
        super(message);
    }

    public CustomRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    public CustomRuntimeException(Throwable cause) {
        super(cause);
    }

    public CustomRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

 

2. ExControllerAdvice

backend/advice/ExControllerAdvice

package mobile.backend.advice;

import lombok.extern.slf4j.Slf4j;
import mobile.backend.exception.CustomRuntimeException;
import mobile.backend.response.CommonResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.NoSuchElementException;

@Slf4j
@RestControllerAdvice(annotations = RestController.class)
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    @ExceptionHandler
    public CommonResponse<String> exHandle(Exception e) {
        log.error("[exceptionHandle] ex", e);
        return new CommonResponse<String>(false, "시스템 오류");
    }

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler
    public CommonResponse<String> customExHandler(CustomRuntimeException e) {
        log.error("[exceptionHandle] ex", e);
        return new CommonResponse<>(false, e.getMessage());
    }

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler
    public CommonResponse<String> customExHandler(NoSuchElementException e) {
        log.error("[exceptionHandle] ex", e);
        return new CommonResponse<>(true, null);
    }
}

 


테스트

Service에서 임의로 에러를 던져보자.

backend/module/user/UserController

@GetMapping
    public CommonResponse<List<User>> findAll() throws Exception {
        return new CommonResponse<List<User>>(true, userService.findAll());
    }

 

backend/module/user/UserService

    public List<User> findAll() throws Exception {
        throw new Exception();
//        return userSpringJPARepository.findAll();
    }
    
        public String save(User user) {
        throw new CustomRuntimeException("요청이 잘못됨");
//        userSpringJPARepository.save(user);
//        return "ok";
    }

 

1. GET Method ( findAll )

 

2. POST Method ( save )

 

원하는 에러 메세지가 정상적으로 출력되었다.

728x90
반응형
728x90
반응형
  • 성공 시 { success : true, result : data }
  • 실패 시 { success : false, result : "에러 사유" }

 

1. CommonResponse

backend/response/CommonResponse

package web.backend.response;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class CommonResponse<T> {
    private boolean success;
    private T result;

}

 

이제 컨트롤러에서 성공 시 반환할 데이터를 수정해보자.

 

2. UserController

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import web.backend.annotation.LogTrace;
import web.backend.module.user.repository.UserUpdateDto;
import web.backend.response.CommonResponse;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping
    public CommonResponse<List<User>> findAll() {
        return new CommonResponse<List<User>>(true, userService.findAll());
    }

    @GetMapping("/{userId}")
    public CommonResponse<User> findByIndexId(@PathVariable(value = "userId") Long userId ) {
        return new CommonResponse<User>(true, userService.findByIndexId(userId));
    }

    @PostMapping
    public CommonResponse<String> save(@RequestBody User user) {
        return new CommonResponse<String>(true, userService.save(user));
    }

    @PatchMapping
    public CommonResponse<String> update(@RequestBody UserUpdateDto user) {
        return new CommonResponse<String>(true, userService.update(user.getId(), user.getUser()));
    }

    @DeleteMapping
    public CommonResponse<String> delete(@RequestParam Long id) {
        return new CommonResponse<String>(true, userService.delete(id));
    }

}

 


테스트

POST Method

 

GET Method

728x90
반응형
728x90
반응형

전체 순서

  1. 인터셉터 ( 요청 객체 로깅 ) 
  2. Basic Aspect ( try catch 에러 핸들링 ) - Order(2) , 모든 controller, service, repository
  3. LogTraceAspect ( log 추적기 ) - Order(1), @LogTrace 어노테이션 지정한 곳
  4. 로직 실행
  5. LogTraceAspect ( log 추적기 ) - Order(1)
  6. Basic Aspect ( try catch 에러 핸들링 ) - Order(2)
  7. 인터셉터 ( 응답 객체 로깅 )

 

LogTrace는 필요한 경우에만 사용하도록 하자.

 

1. BasicAspect 생성

/backend/aop/BasicAspect

package web.backend.aop;


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;

@Slf4j
@Aspect
@Order(2)
public class BasicAspect {

    @Around("execution(* web.backend.module..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

        try {

            Object result = joinPoint.proceed();

            return result;
        } catch (Exception e) {
            throw e;
        }
    }
}

 

2. TraceStatus

backend/aop/trace/TraceStatus

package web.backend.aop.trace;

public class TraceStatus {

    private TraceId traceId;
    private Long startTimeMs;
    private String message;

    public TraceStatus(TraceId traceId, Long startTimeMs, String message) {
        this.traceId = traceId;
        this.startTimeMs = startTimeMs;
        this.message = message;
    }

    public Long getStartTimeMs() {
        return startTimeMs;
    }

    public String getMessage() {
        return message;
    }

    public TraceId getTraceId() {
        return traceId;
    }
}

 

3. TraceId

backend/aop/trace/TraceId

package web.backend.aop.trace;

import java.util.UUID;

public class TraceId {

    private String id;
    private int level;

    public TraceId() {
        this.id = createId();
        this.level = 0;
    }

    private TraceId(String id, int level) {
        this.id = id;
        this.level = level;
    }

    private String createId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    public TraceId createNextId() {
        return new TraceId(id, level + 1);
    }

    public TraceId createPreviousId() {
        return new TraceId(id, level - 1);
    }

    public boolean isFirstLevel() {
        return level == 0;
    }

    public String getId() {
        return id;
    }

    public int getLevel() {
        return level;
    }
}

 

4. LogTrace Interface

backend/aop/trace/logtrace/LogTrace

package web.backend.aop.logtrace;

import hello.proxy.trace.TraceStatus;

public interface LogTrace {

    TraceStatus begin(String message);
    void end(TraceStatus status);
    void exception(TraceStatus status, Exception e);
}

 

5. ThreadLocalLogTrace

backend/aop/trace/logtrace/ThreadLocalLogTrace

package web.backend.aop.trace.logtrace;

import lombok.extern.slf4j.Slf4j;
import web.backend.aop.trace.TraceId;
import web.backend.aop.trace.TraceStatus;

@Slf4j
public class ThreadLocalLogTrace implements LogTrace {

    private static final String START_PREFIX = "-->";
    private static final String COMPLETE_PREFIX = "<--";
    private static final String EX_PREFIX = "<X-";

    private ThreadLocal<TraceId> traceIdHolder = new ThreadLocal<>();

    @Override
    public TraceStatus begin(String message) {
        syncTraceId();
        TraceId traceId = traceIdHolder.get();
        Long startTimeMs = System.currentTimeMillis();
        log.info("[{}] {}{}", traceId.getId(), addSpace(START_PREFIX, traceId.getLevel()), message);

        return new TraceStatus(traceId, startTimeMs, message);
    }

    @Override
    public void end(TraceStatus status) {
        complete(status, null);
    }

    @Override
    public void exception(TraceStatus status, Exception e) {
        complete(status, e);
    }

    private void complete(TraceStatus status, Exception e) {
        Long stopTimeMs = System.currentTimeMillis();
        long resultTimeMs = stopTimeMs - status.getStartTimeMs();
        TraceId traceId = status.getTraceId();
        if (e == null) {
            log.info("[{}] {}{} time={}ms", traceId.getId(), addSpace(COMPLETE_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs);
        } else {
            log.info("[{}] {}{} time={}ms ex={}", traceId.getId(), addSpace(EX_PREFIX, traceId.getLevel()), status.getMessage(), resultTimeMs, e.toString());
        }

        releaseTraceId();
    }

    private void syncTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId == null) {
            traceIdHolder.set(new TraceId());
        } else {
            traceIdHolder.set(traceId.createNextId());
        }
    }

    private void releaseTraceId() {
        TraceId traceId = traceIdHolder.get();
        if (traceId.isFirstLevel()) {
            traceIdHolder.remove();//destroy
        } else {
            traceIdHolder.set(traceId.createPreviousId());
        }
    }

    private static String addSpace(String prefix, int level) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < level; i++) {
            sb.append( (i == level - 1) ? "|" + prefix : "|   ");
        }
        return sb.toString();
    }
}

 

6. LogTrace Annotation

backend/annotation/LogTrace

package web.backend.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTrace {
}

 

7. LogTraceAspect

backend/aop/LogTraceAspect

package web.backend.aop;


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import web.backend.aop.trace.TraceStatus;
import web.backend.aop.trace.logtrace.LogTrace;

@Slf4j
@Aspect
@Order(1)
public class LogTraceAspect {

    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Around("@annotation(web.backend.annotation.LogTrace)")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        TraceStatus status = null;

        try {
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);

            Object result = joinPoint.proceed();

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }

}

 

8. AopConfig 생성

backend/aop/AopConfig

package web.backend.aop;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import web.backend.aop.trace.logtrace.LogTrace;
import web.backend.aop.trace.logtrace.ThreadLocalLogTrace;

@Configuration
public class AopConfig {

    @Bean
    public BasicAspect basicAspect() {
        return new BasicAspect();
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }

    @Bean
    public LogTraceAspect logTraceAspect() {
        return new LogTraceAspect(logTrace());
    }
}

9. WebConfig 등록

backend/WebConfig

package web.backend;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.aop.AopConfig;
import web.backend.interceptor.LogInterceptor;

@Configuration
@Import(AopConfig.class)
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }

}

 

 


테스트

 

save 로직에 @LogTrace 어노테이션을 주고 findAll() 메소드와 비교해보겠다.

 

// controller    
    @PostMapping
    @LogTrace
    public String save(@RequestBody User user) {
        return userService.save(user);
    }
// service
    @LogTrace
    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }
// repository
    @Override
    @LogTrace
    <S extends User> S save(S entity);

 

 

Find 

 

Save

728x90
반응형
728x90
반응형

로깅 내용

  • UUID
  • METHOD
  • URL

 

1. LogInterceptor

backend/interceptor/LogInterceptor

package web.backend.interceptor;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

@Slf4j
@RequiredArgsConstructor
@Component
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uuid = UUID.randomUUID().toString();
        String requestURI = request.getRequestURI();
        String method = request.getMethod();

        request.setAttribute(LOG_ID, uuid);
        log.info("REQUEST  [{}][{}][{}][{}]", uuid, method, requestURI, handler);
        return true;
    }


//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        log.info("postHandle [{}]", modelAndView);
//    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String logId = (String) request.getAttribute(LOG_ID);
        String requestURI = request.getRequestURI();
        String method = request.getMethod();

        log.info("RESPONSE [{}][{}][{}]", logId, method, requestURI);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

 

2. WebConfig 

backend/WebConfig

package web.backend;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import web.backend.interceptor.LogInterceptor;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/css/**", "/*.ico"
                        , "/error", "/error-page/**" //오류 페이지 경로
                );
    }
    
}

 

3. 테스트

 

728x90
반응형
728x90
반응형

1. UserUpdateDto

backend/module/user/repository/userUpdateDto

package web.backend.module.user.repository;

import lombok.Data;
import web.backend.module.user.User;

@Data
public class UserUpdateDto {
    private Long id;
    private User user;
}

 

2. Service

backend/module/user/UserService

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

    public List<User> findAll() {
        return userSpringJPARepository.findAll();
    }

    public User findByIndexId(Long id) {
        return userSpringJPARepository.findById(id).get();
    }

    public String save(User user) {
        userSpringJPARepository.save(user);
        return "ok";
    }

    public String update(Long id, User user) {
        User userOne = userSpringJPARepository.findById(id).get();
        userOne.changeUser(user.getUserId());
        return "ok";
    }

    public String delete(Long id) {
        userSpringJPARepository.deleteById(id);
        return "ok";
    }


}

 

3. Controller

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import web.backend.module.user.repository.UserUpdateDto;

import java.util.List;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    @GetMapping
    public List<User> findAll() {
        return userService.findAll();
    }

    @GetMapping("/{userId}")
    public User findByIndexId(@PathVariable(value = "userId") Long userId ) {
        return userService.findByIndexId(userId);
    }

    @PostMapping
    public String save(@RequestBody User user) {
        return userService.save(user);
    }

    @PatchMapping
    public String update(@RequestBody UserUpdateDto user) {
        return userService.update(user.getId(), user.getUser());
    }

    @DeleteMapping
    public String delete(@RequestParam Long id) {
        return userService.delete(id);
    }

}

 

 


테스트 코드 작성

 

/src/test/java/web/backend/module/user/UserServiceTest

package web.backend.module.user;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.After;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

import javax.persistence.EntityManager;

import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Slf4j
class UserServiceTest {

    @Autowired
    EntityManager em;

    @Autowired
    UserSpringJpaRepository userSpringJpaRepository;

    @Autowired
    UserQueryRepository userQueryRepository;

    @BeforeEach
    public void before() {
        User user = new User("userId");
        User savedUser = userSpringJpaRepository.save(user);
    }

    @AfterEach
    public void after() {
        em.flush();
        em.clear();
    }


    @Test
    @Transactional
    void save() {
        Optional<User> findUser = userSpringJpaRepository.findByUserId("userId");
        String userId = findUser.get().getUserId();

        assertEquals("userId",userId);
    }

    @Test
    @Transactional
    void find() {

        List<User> findAll = userSpringJpaRepository.findAll();

        for( User userOne : findAll) {
            assertEquals("userId",userOne.getUserId());
        }

        User findOne = userSpringJpaRepository.findByUserId("userId").get();
        assertEquals("userId",findOne.getUserId());
    }

    @Test
    @Transactional
    void update() {
        Optional<User> findUser = userSpringJpaRepository.findByUserId("userId");
        User userOne = findUser.get();
        userOne.changeUser("newUserId");

        Optional<User> findNewUser = userSpringJpaRepository.findByUserId("newUserId");

        assertEquals("newUserId",findNewUser.get().getUserId());
    }

    @Test
    @Transactional
    void delete() {
        userSpringJpaRepository.deleteById(1L);

        assertEquals(0,userSpringJpaRepository.findAll().size());
    }


}

 

 

 

728x90
반응형
728x90
반응형

본 프로젝트는 Querydsl, SpringDataJpa 둘 다 사용 가능하게끔 설계했습니다.

 

1. 파일 만들기

2. User DAO

backend/module/user/User

package web.backend.module.user;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@Table(name="user_info")
public class User {

    @Id @GeneratedValue
    @Column(name="user_index")
    private Long id;

    @Column(name="user_id")
    private String userId;

    @CreatedDate
    @Column(name="user_createdat", updatable = false)
    private LocalDateTime user_creadtedat;

    @LastModifiedDate
    @Column(name="user_updatedat")
    private LocalDateTime user_updatedat;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        user_creadtedat = now;
        user_updatedat = now;
    }

    @PreUpdate
    public void preUpdate() {
        user_updatedat = LocalDateTime.now();
    }

    public User(String userId) {
        this.userId = userId;
    }

    /**
     * user 수정
     */
    public void changeUser(String userId) {
        this.userId = userId;
    }

}

 

3. UserController

backend/module/user/UserController

package web.backend.module.user;

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

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

}

 

4. UserService

backend/module/user/UserController

package web.backend.module.user;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import web.backend.module.user.repository.UserQueryRepository;
import web.backend.module.user.repository.UserSpringJpaRepository;

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class UserService {

    private final UserQueryRepository userQueryRepository;
    private final UserSpringJpaRepository userSpringJPARepository;

}

 

5. UserQueryRepository

backend/module/user/repository/UserQueryRepository

package web.backend.module.user.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;

@Repository
@Slf4j
public class UserQueryRepository {

    private final JPAQueryFactory query;

    public UserQueryRepository(EntityManager em) {
        this.query = new JPAQueryFactory(em);
    }
}

 

6. UserSpringJpaRepository

backend/module/user/repository/UserSpringJpaRepository

package web.backend.module.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import web.backend.module.user.User;

import java.util.Optional;

public interface UserSpringJpaRepository extends JpaRepository<User, Long> {

    Optional<User> findByUserId(String userId);
}

 

728x90
반응형
728x90
반응형

스프링 부트에 대해 기능별로는 검색하면 나오는 정보가 많지만 전체적인 환경을 세팅하는게 생각보다 찾기 힘들었다.

한 달동안 본 강의를 토대로 내 입맛대로 환경세팅을 해보기로 했다.

 

1. Java 11, IntelliJ 설치

https://www.oracle.com/java/technologies/downloads/#java11-mac

 

Download the Latest Java LTS Free

Subscribe to Java SE and get the most comprehensive Java support available, with 24/7 global access to the experts.

www.oracle.com

 

https://www.jetbrains.com/idea/download/#section=mac

 

Download IntelliJ IDEA: The Capable & Ergonomic Java IDE by JetBrains

Download the latest version of IntelliJ IDEA for Windows, macOS or Linux.

www.jetbrains.com

 

2. H2 Database 설치

https://www.h2database.com 

 

H2 Database Engine (redirect)

H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo

www.h2database.com

 

해당 경로로 이동하여 권한 설정 ( /bin )

$ chmod 755 build.sh

 

실행

$ ./h2.sh

 

home 경로에 test.mv.db 파일 생성 확인

 

 

2. 프로젝트 생성 

( Dependencies 는 프로젝트의 환경에 맞게 설정한다. )

 

https://start.spring.io/

 

3. Annotation Processing 설정

 

4. build.gradle 설정

plugins {
	id 'org.springframework.boot' version '2.7.4'
	id 'io.spring.dependency-management' version '1.0.14.RELEASE'
	id 'java'
}

group = 'web'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

// Querydsl
ext["hibernate.version"] = "5.6.5.Final"
clean {
	delete file('src/main/generated')
}

dependencies {
	// start.spring.io setting
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// Querydsl
	implementation 'com.querydsl:querydsl-jpa'
	annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jpa"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"

	// app
	implementation 'org.springframework.boot:spring-boot-starter-aop'

	// 테스트에서 lombok 사용
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
    
    //redis
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'

	// 강제지연로딩
	//implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'

	// SQL 실행 파라미터를 로그로 남긴다.
	//implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

5. application.properties 설정

#port
server.port=8083

spring.profiles.active=local

#Database
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.hikari.connection-timeout= 3000
spring.datasource.hikari.validation-timeout= 3000
spring.datasource.hikari.minimum-idle= 5
spring.datasource.hikari.max-lifetime= 240000
spring.datasource.hikari.maximum-pool-size= 20

# 자동 카멜 표기법 x
#spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

# ?? ????? validate or none
spring.jpa.hibernate.ddl-auto=create

# Hibernate jpa log (ddl-auto, ?? jpa ?)
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

# JPA SQL
# logging.level.org.hibernate.SQL=DEBUG

# sql parameter log
# logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

# JPA Transaction log
# logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
# logging.level.org.hibernate.resource.transaction=DEBUG

# Interceptor transaction log
# logging.level.org.springframework.transaction.interceptor=TRACE
# logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager= DEBUG

# JPQL log(queryDSL)
# spring.jpa.properties.hibernate.use_sql_comments=TRUE

# Page_batch_size
# spring.data.web.pageable.default-page-size=20
# spring.data.web.pageable.max-page-size=2000

 

maraiDB 설정

spring.datasource.driverClassName=org.mariadb.jdbc.Driver

spring.datasource.url=jdbc:mariadb://localhost:3307/leafCat

spring.datasource.username=root

spring.datasource.password=root

6. 로그 파일 저장

 

/src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
 [Layout]
 %m : 로그내용이 출력
 %p : trace > debug > info > warn > error 등의 priority 출력
 %r : 어플리케이션이 시작되어 로깅이벤트가 발생하는 시점까지의 경과시간을 밀리세컨드로 출력
 %c : 예) 카테고리가 a.b.c 처럼 되어있다면 %c{2}는 b.c가 출력됩니다.
 %n :  플랫폼 종속적인 개행문자가 출력된다. \r\n 또는 \n 일것이다
 %d : 로깅이벤트가 일어나 날짜 출력 ( 프로그램의 실행속도를 느리게 한다.)
     예) %d{HH:mm:ss} 또는 %d{dd MMMM yyyy HH:mm:ss}
 %C : 호출자의 클래스명 출력
    예) 클래스구조가 org.apache.xyz.SomeClass 처럼 되어있다면 %C{2}는 xyz.SomeClass 가 출력됩니다
 %M : 로깅이 발생한 method 이름을 나타냅니다.
 %F : 로깅이 발생한 프로그램 파일명을 나타냅니다.
 %l : 로깅이 발생한 caller의 정보를 나타냅니다
 %L : 로깅이 발생한 caller의 라인수를 나타냅니다
 %x : 로깅이 발생한 thread와 관련된 NDC(nested diagnostic context)를 출력합니다.
 %X : 로깅이 발생한 thread와 관련된 MDC(mapped diagnostic context)를 출력합니다.
 %% : % 표시를 출력하기 위해 사용한다.
 %t : 로그이벤트가 발생된 쓰레드의 이름을 출력합니다
-->
<configuration scan="true" scanPeriod="30 seconds">

    <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- rollover daily -->
            <fileNamePattern>logs\%d{yyyy-MM}\%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 100MB -->
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{HH:mm:ss.SSS} [%-5level] - %msg%n</Pattern>
        </layout>
    </appender>

    <!-- Loggers -->
    <!-- <logger name="org.apache.catalina" level="ERROR">
    </logger>

    <logger name="org.apache.commons" level="ERROR">
    </logger>

    <logger name="org.springframework" level="DEBUG" >
    </logger>

    <logger name="egovframework.*" level="DEBUG">
    </logger>

    <logger name="java.sql" level="DEBUG">
    </logger>

    <logger name="org.mybatis.spring" level="DEBUG">
    </logger>

    <logger name="egovframework.sqlmappers" level="TRACE">
    </logger> -->

    <root level="INFO">
        <appender-ref ref="ROLLING"/>
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

 

728x90
반응형
728x90
반응형

1. 컨트롤러 매핑 ( value 지정, 조건 추가 )

package hello.springmvc.basic.requestmapping;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

@RestController
public class MappingController {

    private Logger log = LoggerFactory.getLogger(getClass());

    /**
     * method 특정 HTTP 메서드 요청만 허용
     * GET, HEAD, POST, PUT, PATCH, DELETE
     */
    @RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info("mappingGetV1");
        return "ok";
    }


    /**
     * 편리한 축약 애노테이션 (코드보기) * @GetMapping
     *
     * @PostMapping
     * @PutMapping
     * @DeleteMapping
     * @PatchMapping
     */
    @GetMapping(value = "/mapping-get-v2")
    public String mappingGetV2() {
        log.info("mapping-get-v2");
        return "ok";
    }


    /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     *
     * @PathVariable("userId") String userId -> @PathVariable userId
     */
    @GetMapping("/mapping/{userId}")
    public String mappingPath(@PathVariable("userId") String data) {
        log.info("mapping Path userId={}", data);
        return "ok";
    }


    /**
     * PathVariable 사용 다중
     */
    @GetMapping("/mapping/users/{userId}/orders/{orderId}")
    public String mappingPath(@PathVariable String userId, @PathVariable Long
            orderId) {
        log.info("mappingPath userId={}, orderId={}", userId, orderId);
        return "ok";
    }

    /**
     * 파라미터로 추가 매핑
     * params="mode",
     * params="!mode"
     * params="mode=debug"
     * params="mode!=debug" (! = )
     * params = {"mode=debug","data=good"}
     */
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }

    /**
     * 특정 헤더로 추가 매핑
     * headers="mode",
     * headers="!mode"
     * headers="mode=debug"
     * headers="mode!=debug" (! = )
     */
    @GetMapping(value = "/mapping-header", headers = "mode=debug")
    public String mappingHeader() {
        log.info("mappingHeader");
        return "ok";
    }

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * consumes="application/json"
     * consumes="!application/json"
     * consumes="application/*"
     * consumes="*\/*"
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = "/mapping-consume", consumes = "application/json")
    public String mappingConsumes() {
        log.info("mappingConsumes");
        return "ok";
    }

    /**
     * Accept 헤더 기반 Media Type
     * produces = "text/html"
     * produces = "!text/html"
     * produces = "text/*"
     * produces = "*\/*"
     */
    @PostMapping(value = "/mapping-produce", produces = "text/html")
    public String mappingProduces() {
        log.info("mappingProduces");
        return "ok";
    }

}

 

2. CRUD 매핑 ( value 생략 )

package hello.springmvc.basic.requestmapping;

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

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    /**
     * GET /mapping/users
     */
    @GetMapping
    public String users() {
        return "get users";
    }

    /**
     * POST /mapping/users
     */
    @PostMapping
    public String addUser() {
        return "post user";
    }

    /**
     * GET /mapping/users/{userId}
     */
    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId=" + userId;
    }

    /**
     * PATCH /mapping/users/{userId}
     */
    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "update userId=" + userId;
    }

    /**
     * DELETE /mapping/users/{userId}
     */
    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId=" + userId;
    }
}

 

3. header 매핑 ( Controller 함수 매개변수 )

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

@Slf4j
@RestController
@RequestMapping("/headers")
public class RequestHeaderController {

    @RequestMapping
    public String headers(
            HttpServletRequest request,
            HttpServletResponse response,
            HttpMethod httpMethod,
            Locale locale,
            @RequestHeader MultiValueMap<String, String> headerMap,
            @RequestHeader("host") String host,
            @CookieValue(value = "myCookie", required = false) String cookie

            ) {

        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "OK";
    }

}

 

4. RequestParam

package hello.springmvc.basic.request;

import hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

@Slf4j
@Controller
public class RequestParamController {
    /**
     * 반환 타입이 없으면서 이렇게 응답에 값을 직접 집어넣으면, view 조회X
     */
    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        log.info("username={}, age={}", username, age);
        response.getWriter().write("ok");
    }

    @ResponseBody
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String username,
            @RequestParam("age") int age
    ) throws IOException {
        log.info("username={}, age={}", username, age);
        return "OK";
    }

    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age
    ) throws IOException {
        log.info("username={}, age={}", username, age);
        return "OK";
    }

    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age){
        log.info("username={}, age={}", username, age);
        return "OK";
    }

    /**
     * @RequestParam.required
     * /request-param-required -> username이 없으므로 예외 *
     * 주의!
     * /request-param-required?username= -> 빈문자로 통과 *
     * 주의!
     * /request-param-required
     * int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는
    defaultValue 사용) */
    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(
            @RequestParam(required = true) String username,
            @RequestParam(required = false) Integer age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

    /**
     * @RequestParam
     * - defaultValue 사용 *
     * 참고: defaultValue는 빈 문자의 경우에도 적용 * /request-param-default?username=
     */
    @ResponseBody
    @RequestMapping("/request-param-default")
    public String requestParamDefault(
            @RequestParam(required = true, defaultValue = "guest") String username,
            @RequestParam(required = false, defaultValue = "-1") int age) {
        log.info("username={}, age={}", username, age);
        return "ok";
    }

    /**
     * @RequestParam Map, MultiValueMap
     * Map(key=value)
     * MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
     */
    @ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
        return "ok";
    }

    /**
     * @ModelAttribute 사용
     * 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때
    자세히 설명
     */
    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(),
                helloData.getAge());
        return "ok";
    }

    /**
     * @ModelAttribute 사용
     * 참고: model.addAttribute(helloData) 코드도 함께 자동 적용됨, 뒤에 model을 설명할 때
    자세히 설명
     */
    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2( HelloData helloData) {
        log.info("username={}, age={}", helloData.getUsername(),
                helloData.getAge());
        return "ok";
    }

}

 

5. RequestBody - String

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;

@Slf4j
@Controller
public class RequestBodyStringController {

    @PostMapping("/request-body-string-v1")
    public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}",messageBody);

        response.getWriter().write("ok");
    }


    /**
     * InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회 * OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력 */
    @PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
        String messageBody = StreamUtils.copyToString(inputStream,StandardCharsets.UTF_8);
        log.info("messageBody={}", messageBody);
        responseWriter.write("ok");
    }

    /**
     * HttpEntity: HTTP header, body 정보를 편리하게 조회
     * - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
     * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 *
     * 응답에서도 HttpEntity 사용 가능
     * * */

    @PostMapping("/request-body-string-v3")
    public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
        String messageBody = httpEntity.getBody();
        log.info("messageBody={}", messageBody);

        return new HttpEntity<>("ok");
    }

    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);
        return "ok";
    }
}

 

6. Response Body - Json

package hello.springmvc.basic.request;

import com.fasterxml.jackson.databind.ObjectMapper;
import hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Slf4j
@Controller
public class RequestBodyJsonController {
    private ObjectMapper objectMapper = new ObjectMapper();

    @ResponseBody
    @PostMapping("/request-body-json-v1")
    public String requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody = {}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "OK";
    }

    /**
     * @RequestBody HttpMessageConverter 사용 -> StringHttpMessageConverter 적용 *
     * @ResponseBody - 모든 메서드에 @ResponseBody 적용
     * - 메시지 바디 정보 직접 반환(view 조회X)
     * - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
     */
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {

        log.info("messageBody = {}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }

    /**
     * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
     * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-
     * type: application/json)
     */
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

    @ResponseBody
    @PostMapping("/request-body-json-v4")
    public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
        HelloData data = httpEntity.getBody();
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

    /**
     * @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
     * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content- type: application/json)
     * @ResponseBody 적용
     * - 메시지 바디 정보 직접 반환(view 조회X)
     * - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용
     * (Accept: application/json)
     */
    @ResponseBody
    @PostMapping("/request-body-json-v5")
    public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return data;
    }
}
728x90
반응형

+ Recent posts