스터디 레포트 202403 #2

Table of Contents

[TOC]

[ ←전편보기 / 1편, 3편, 4편, 5편, 6편 / 다음편보기→ ]

결과물 git주소 : https://gitlab.ntiple.com/developers/study-202403

github : https://github.com/lupfeliz/study-202403

1. 개요

지난 1차 레포트에 이은 스터디 내용 정리.

이번 편에서는 백엔드 (Java - WAS) 어플리케이션을 작성해 볼거다

아무것도 없는 상태에서 (from-scratch) 시작해서 (그래도 javagradle 은 가지고 있어야 한다.)

불필요한 기능들을 최소화 하고. 완전히 구동되는 Springboot-Application 을 만들어 보는것을 목표로 한다.

Java-17 : https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
Gradle-8.8 : https://gradle.org/next-steps/?version=8.8&format=bin

2. Springboot Application 기초 프로젝트 생성

먼저 설치된 gradle 을 사용해 아래와 같이 gradle init --type basic --dsl kotlin 을 실행한다

(필자의 경우 D:\Applications\gradle-8.8 에 gradle이 설치되어 있었다)

> D:\Applications\gradle-8.8\bin\gradle init --type basic --dsl kotlin

Project name (default: mywas): 

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] 

> Task :init
To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.2.1/samples

BUILD SUCCESSFUL in 5s
2 actionable tasks: 2 executed

> mkdir libs
> mkdir src\main\java
> mkdir src\main\resources
> mkdir src\test\java
> mkdir src\test\resources
> mkdir src\main\java\my\was\mywas

  • build.gradle.kts 파일을 수정하여 필요한 라이브러리 들의 의존설정을 수행한다.

plugins {
  id("java")
  id("war")
  /** spring 관련 */
  id("org.springframework.boot") version "3.2.4"
  id("io.spring.dependency-management") version "1.1.4"
  id("io.freefair.lombok") version "8.1.0"
}

group = "my.was"
version = "0.0.1"

java {
  sourceCompatibility = JavaVersion.VERSION_17
  targetCompatibility = JavaVersion.VERSION_17
}

configurations {
  compileOnly {
    extendsFrom(configurations.annotationProcessor.get())
  }

  all {
    exclude("commons-logging:commons-logging")
    exclude("org.apache.tomcat.embed:tomcat-embed-el")
    exclude("org.apache.tomcat.embed:tomcat-embed-websocket")
  }
}

repositories {
  mavenCentral()
  maven(url = "https://repo.spring.io/milestone")
  maven(url = uri("https://nexus.ntiple.com/repository/maven-public/")).isAllowInsecureProtocol = true
}

dependencies {
  /** lombok 관련 */
  compileOnly("org.projectlombok:lombok")
  annotationProcessor("org.projectlombok:lombok")

  /** spring 베이스 */
  implementation("org.springframework.boot:spring-boot-starter-web")
  implementation("org.springframework.boot:spring-boot-starter-data-jpa")
  implementation("org.springframework.boot:spring-boot-starter-security")

  providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")

  /** 기타 필요사항들 */
  implementation("commons-codec:commons-codec:1.15")
  implementation("com.ntiple:ntiple-utils:0.0.2-10")
  implementation("javax.validation:validation-api:2.0.1.Final")
  implementation("org.apache.httpcomponents:httpclient:4.5.14")
  implementation("org.apache.httpcomponents:httpmime:4.5.14")

  /** OpenAPI (swagger) */
  implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")

  /** 런타임 및 개발관련 */
  developmentOnly("org.springframework.boot:spring-boot-devtools")
  developmentOnly("org.springframework.boot:spring-boot-starter-tomcat")
  runtimeOnly("com.h2database:h2")

  /** jwt */
  implementation("io.jsonwebtoken:jjwt-api:0.11.5")
  runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
  runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
  /** yml, json */
  implementation("org.yaml:snakeyaml:2.2")
  implementation("org.json:json:20230618")
  /** 이메일 */
  implementation("com.sun.mail:jakarta.mail:2.0.1")
  /** 로그 */
  implementation("org.logback-extensions:logback-ext-spring:0.1.5")
  /** 기타 libs 폴더에 있는 jar 파일들 */
  implementation (fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))

  /** 테스트관련 */
  testCompileOnly("org.projectlombok:lombok")
  testAnnotationProcessor("org.projectlombok:lombok")
  testImplementation("com.ntiple:ntiple-utils:0.0.2-9")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
}

task("prebuildHook") {
  dependsOn(tasks.named("build"))
  dependsOn(tasks.named("bootRun"))
}

tasks {
  withType<JavaExec> {
    var profile = System.getProperty("spring.profiles.active")
    if (profile == null || "".equals(profile)) { profile = "local" }
    System.setProperty("spring.profiles.active", profile);
    systemProperty("spring.profiles.active", profile)
  }
  named<Test>("test") {
    useJUnitPlatform()
  }
  named<JavaExec>("bootRun") {
    var profile = System.getProperty("spring.profiles.active")
    println("================================================================================")
    println("데모 API")
    println("PROFILE:" + profile)
    println("================================================================================")
  }
}

  • src/main/resources/application.yml 을 생성한다.

debug: false
server:
  port: 8080
  forward-headers-strategy: FRAMEWORK
spring:
  application:
    name: demo
  mvc:
    static-path-pattern: "/**"
  web:
    resources:
      static-locations: "classpath:/static/"
  servlet:
    multipart:
      maxFileSize: "1GB"
      maxRequestSize: "1GB"
  datasource:
    driverClassName: org.h2.Driver
    ##  메모리 전용 DB 사용시 아래 설정 사용
    # url: jdbc:h2:mem:test
    url: jdbc:h2:./demo-app.db
    username: sa
    password: 
  jpa:
    defer-datasource-initialization: true 
    generate-ddl: true
    show-sql: false
    properties:
      hibernate:
        dialect: ""
        ddl-auto: update
        format-sql: false
    mapping-resources:
  ## H2 Console 설정
  h2:
    console:
      enabled: true
      path: /h2-console
      settings:
        web-allow-others: true

security:
  jwt:
    ## 액세스토큰
    access:
      algorhithm: HmacSHA512
      ## 액세스토큰 발급용 키 (TODO: 추후 keygen 사용)
      secret: "t28wa4Pak6PAuQ1Ps2ycKRoBROybGBFzzqki3zGjk6TgTFhPutuQB3nSosLuIw4Yyuv7eSAdCfTVz0kfYi+Myw=="
      ## 1000 * 60 * 30 = 30분
      expire: 1802000
    ## 리프레시토큰
    refresh:
      algorhithm: HmacSHA512
      ## 리프레시토큰 발급용 키 (TODO: 추후 keygen 사용)
      secret: "v1MzR2oTSvn7Gio5TkUO/rd2teJlwkRUWh0mlkBrPLlxBso/U6ZOSz4Y5QMGa23aIB66ykZIqn1DFJodhPwB1w=="
      ## 1000 * 60 * 60 * 24 = 1일
      expire: 86402000
  key:
    aes:
      secret: "Fce2YOFk/TVcJfAFVZqnL9SWHfkxQEA47g0nk2GxpMM="
    rsa:
      privatekey: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMhfVP50PfMYry6YsMYpEdTPjWRHflvUNDhzhrgyO5gEiTIj2lK6S95XRggsU/NGTbRqRouBmFlV4+KHL9LLKhXszqlDRDWf1jZJBNdI4/GpRlQU/MkByji+2lv9894gJPXpExDLO/bM+PTZ16HMAMyZJ6DNw5hD/GMEudZk6U1lAgMBAAECgYAD/17nOrN3s6DfGZ3BPlWEPOXRv9lmBJxMGgXwi9QDiuefz/ZNmzjjRTN4+0Vrf5YSSOKCawH6mkuTG+ZY2sPKpiIBSX4SXewMIKRhbxOj19iwrNp1gBDK3s/kpHy9+x8b7tuEIITNYreuadYAvVgSMlaJVPdh7uUm39sMnn/aqwJBANRWQBpaOfw3iQ+g7jBrBJz+e5QbmsNZeNUPrk9WbMuhKQkLUP2E4U5noFKV8SZFGo48YPKsC32DxFF3e8kw2fsCQQDxk0ALLlepzdpqVkm6YQ4KtTEweug+L/RdjU5apGZLZI+AGfYIE1JIzyzgdPke92ePnw1wPqFgi0PyzyJn8DgfAkAq5M2IRUfHapSWgqT7RPMmn8XpEnZ+Ffnx2HwW7NeHfyPh/tY6kHhPNWHOrRmM6JLHvuy6uQSNM2waJO/toZ+3AkEAo4WzUl46RNztPhHOsnTEFod0Fob78ixv02u1YDHsdJhLcsEgA3NgvZxPmlhT0ZxS46scY6BhiIJ8qj1/4q9+rQJAbZj4vsQB5SrKcYNR8VVYMP9EYXnFax66QgSjGI3zPJhjAcbdyhDVshFKHcsI3R1dxegLFTbvjYKejzWfHBcBUQ=="
      publickey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIX1T+dD3zGK8umLDGKRHUz41kR35b1DQ4c4a4MjuYBIkyI9pSukveV0YILFPzRk20akaLgZhZVePihy/SyyoV7M6pQ0Q1n9Y2SQTXSOPxqUZUFPzJAco4vtpb/fPeICT16RMQyzv2zPj02dehzADMmSegzcOYQ/xjBLnWZOlNZQIDAQAB"

## OPEN-API 설정
springdoc:
  packages-to-scan: my.was.mywas
  default-consumes-media-type: application/json; charset=UTF-8
  default-produces-media-type: application/json; charset=UTF-8
  swagger-ui:
    path: /swagger/
    disable-swagger-default-url: true
    display-request-duration: true
    operations-sorter: alpha

logging:
  level:
    org.springframework.boot: INFO

  • src/test/resources/logback.xml 을 생성한다. (JUNIT 테스트용 로그설정 / 필요없는경우 생략)

<configuration scan="true" scanPeriod="10 seconds">
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>[%date %level] \(%file:%line\) %msg%n</pattern>
    </encoder>
  </appender>
  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

  • src/main/java/my/was/mywas/MyWasApplication.java 을 생성한다. (스프링부트 실행 클래스)

package my.was.mywas;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyWasApplication {
  public static void main(String[] args) {
    String profile = System.getProperty("spring.profiles.active");
    if (profile == null || "".equals(profile)) {
      System.setProperty("spring.profiles.active", "local");
    }
    SpringApplication.run(MyWasApplication.class, args);
  }
}

  • src/main/java/my/was/mywas/ServletInitializer.java 을 생성한다. (tomcat 등 was container 에서 실행할 경우 필요)

package my.was.mywas;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {
  @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(MyWasApplication.class);
  }
}

  • src/main/java/my/was/mywas/configs/SecurityConfig.java 을 생성한다 (Spring Security)

package my.was.mywas.configs;

import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;

... 중략 ...

@Slf4j @Configuration
@EnableWebSecurity @EnableMethodSecurity 
public class SecurityConfig {

  @Autowired private CorsFilter corsFilter;
  @Autowired private AuthFilter authFilter;
  @Autowired private JwtAuthenticationEntryPoint authPoint;
  @Autowired private JwtAccessDeniedHandler authHandler;

  /** URL 패턴매칭 */
  private static AntPathRequestMatcher matcher(HttpMethod m, String path) {
    if (m != null) {
      return AntPathRequestMatcher.antMatcher(m, path);
    } else {
      return AntPathRequestMatcher.antMatcher(path);
    }
  }

  @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    /** 전체공개 (인증없이 접근 가능한 목록) */
    List<RequestMatcher> reqPubLst = new ArrayList<>();
    /** 회원 사용자만 허용 */
    List<RequestMatcher> reqMbrLst = new ArrayList<>();
    /** 웹리소스 */
    List<RequestMatcher> reqWebLst = new ArrayList<>();

    reqPubLst.addAll(List.of(
      /** GET /api/cmn/* (공용API) */
      matcher(GET, "/api/cmn/**"),
      /** GET /api/usr/usr01001a01/** (마이페이지) */
      matcher(GET, "/api/usr/usr01001/**"),
      /** PUT /api/usr/** (회원가입) */
      matcher(PUT, "/api/usr/**"),
      /** POST /api/lgn/ (로그인) */
      matcher(POST, "/api/lgn/**"),
      /** POST /api/atc/atc01001/ (게시물 검색) */
      matcher(POST, "/api/atc/atc01001"),
      /** POST /api/atc/atc01001/ (게시물 상세조회) */
      matcher(GET, "/api/atc/atc01001/**"),
      /** H2DB웹콘솔 */
      matcher(null, "/h2-console/**"),
      /** 스웨거(OPENAPI) */
      matcher(null, "/swagger/swagger-ui/**"),
      matcher(null, "/swagger/swagger-resources/**"),
      matcher(null, "/swagger/v3/api-docs/**")
    ));

    /** 기타 /api 로 시작되는 모든 리퀘스트 들은 권한 필요 */
    reqMbrLst.addAll(List.of(
      matcher(null, "/api/**")
    ));

    reqWebLst.addAll(List.of(
      /** 기타 GET 메소드로 접근하는 모든 웹 리소스 URL */
      matcher(GET, "/**")
    ));

    final RequestMatcher[] reqPub = reqPubLst.toArray(new RequestMatcher[]{ });
    final RequestMatcher[] reqMbr = reqMbrLst.toArray(new RequestMatcher[]{ });
    final RequestMatcher[] reqWeb = reqWebLst.toArray(new RequestMatcher[]{ });
    log.debug("PUBLIC-ALLOWED:{}{}", "", reqPub);
    http
      /** token을 사용하는 방식이므로 csrf disable */ 
      .csrf(csrf -> csrf.disable())
      .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
      .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
      .exceptionHandling(exh -> exh
        /** 인증 실패 핸들링 */
        .authenticationEntryPoint(authPoint)
        /** 권한인가실패 핸들링 */
        .accessDeniedHandler(authHandler)
      )
      .headers(hdr ->
        hdr.frameOptions(frm -> frm.sameOrigin())
          /** 동일 사이트 referer */
          .referrerPolicy(ref -> ref.policy(ReferrerPolicy.SAME_ORIGIN))
          /** xss 보호 */
          .xssProtection(xss -> xss.disable())
      )
      /** 세션 사용하지 않음 */
      .sessionManagement(mng -> mng
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      /** URI별 인가설정 */
      .authorizeHttpRequests(req -> req
        // .anyRequest().permitAll()
        /** 전체공개 (인증없이 접근 가능한 목록) */
        .requestMatchers(reqPub).permitAll()
        /** 회원 사용자만 허용 */
        .requestMatchers(reqMbr).hasAnyAuthority("ROLE_USER")
        /** 웹리소스 */
        .requestMatchers(reqWeb).permitAll()
        .anyRequest()
          .hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
      )
      /** 폼 로그인 불가 */
      .formLogin(login -> login.disable())
      /** 폼 로그아웃 불가 */
      .logout(logout -> logout.disable())
      /** 임의유저 불가 */
      .anonymous(anon -> anon.disable())
      ;
    SecurityFilterChain ret = http.build();
    return ret;
  }

  @Component public static class AuthFilter extends GenericFilterBean {
    @Override public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain)
      throws IOException, ServletException {
      /** 추후 Response 객체를 Request 에서 읽어올 수 있도록 저장 */
      HttpServletRequest req = (HttpServletRequest) sreq;
      req.setAttribute(HttpServletResponse.class.getName(), sres);
      /** TODO: 토큰유무 및 인증정보 확인 */
      log.debug("TODO: 토큰유무 및 인증정보 확인");
      chain.doFilter(sreq, sres);
    }
  }

  /** 인증 실패 핸들링 */
  @Component public static class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override public void commence(HttpServletRequest req, HttpServletResponse res,
      AuthenticationException e) throws IOException, ServletException {
      log.debug("AUTH-ERR:{} {}", req.getRequestURI(), e.getMessage());
      res.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.name());
    }
  }

  /** 권한인가실패 핸들링 */
  @Component public static class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override public void handle(HttpServletRequest req, HttpServletResponse res,
      AccessDeniedException e) throws IOException, ServletException {
      log.debug("ACCESS-DENIED:{}", e.getMessage());
      res.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.name());
    }
  }

  /** Cross Origin Resource Sharing 필터링 (일단은 전부허용)  */
  @Configuration public static class CorsFilterConfig {
    @Bean public CorsFilter corsFilter() {
      UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
      CorsConfiguration cfg = new CorsConfiguration();
      cfg.setAllowCredentials(true);
      cfg.addAllowedOriginPattern("*");
      cfg.addAllowedHeader("*");
      cfg.addAllowedMethod("*");
      source.registerCorsConfiguration("/api/**", cfg);
      return new CorsFilter(source);
    }
  }
}

  • src/main/java/my/was/mywas/configs/OpenAPIConfig.java 를 생성한다. (SWAGGER)

package my.was.mywas.configs;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;

@OpenAPIDefinition(servers = { @Server(url = "/", description = "기본URL") })
@Configuration public class OpenAPIConfig {
  @Bean public OpenAPI customOpenAPI() {
    final String secureScheme = "인증전송자(Bearer)";
    return new OpenAPI()
      .addSecurityItem(new SecurityRequirement().addList(secureScheme))
      .components(new Components()
        .addSecuritySchemes(secureScheme, new SecurityScheme()
          .name(secureScheme)
          .type(SecurityScheme.Type.HTTP)
          .scheme("bearer")
          .bearerFormat("JWT")))
      .info(new Info().title("데모 프로그램"));
  }
}

  • 여기까지 수행하면 아래와 같은 구조가 만들어진다.

├── bin [dir]
├── build [dir]
├── gradle [dir]
├── build.gradle.kts
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    │   ├── java
    │   │   └── my
    │   │       └── was
    │   │           └── mywas
    │   │               ├── configs
    │   │               │   ├── OpenAPIConfig.java
    │   │               │   └── SecurityConfig.java
    │   │               ├── MyWasApplication.java
    │   │               └── ServletInitializer.java
    │   └── resources
    │       └── application.yml
    └── test
        ├── java
        └── resources
            └── logback.xml

  • 실행은 sh gradlew bootRun 으로 실행해 볼 수 있다

> gradlew bootRun

> Configure project :
================================================================================
데모 API
PROFILE:local
================================================================================

> Task :bootRun
Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts
Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.4)
... 중략 ...
erAwareRequestFilter@53ef09f, org.springframework.security.web.session.SessionManagementFilter@11a02d20, org.springframework.security.web.access.ExceptionTranslationFilter@4a136d3a, org.springframework.security.web.access.intercept.AuthorizationFilter@477d5d3f
2024-06-16T02:57:56.836+09:00  INFO 34665 --- [demo] [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2024-06-16T02:57:56.870+09:00  INFO 34665 --- [demo] [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-06-16T02:57:56.883+09:00  INFO 34665 --- [demo] [  restartedMain] my.was.mywas.MyWasApplication            : Started MyWasApplication in 3.23 seconds (process running for 3.583)
<==========---> 83% EXECUTING [49s]
> :bootRun

  • 브라우저로 실행해 보면 아래와 같은 화면이 출력된다

OPEN-API (SWAGGER) : http://localhost:8080/swagger/swagger-ui/index.html
H2-DB CONSOLE : http://localhost:8080/h2-console

  • 일단 이로서 기초 공사는 마쳤다!고 할수 있겠다 이제 각종 공통 요소부터 구현한다.

3. 공통 요소들 작성

본격적으로 비즈니스로직을 작성하기 이전에 로직을 간편화 시켜줄 수 있는 공통 유틸리티 들을 작성하여 사용한다.

보통 프로젝트에서 이 구간에 공을 얼마나 들이냐에 따라 프로젝트 복잡도 / 난이도가 꽤나 차이나게 된다.

프레임 워크 위의 또다른 프레임 워크 라고 보면 이해가 빠를 듯 하다.

따라서 이 부분은 프로젝트별로 천차만별 이며, 알기 힘든 부분도 많다.

일단은 필자가 아는 내용대로(내맘대루) 작성하겠다.

3-1. 공통상수 (Constants.java)

  • 공통으로 사용하는 상수들을 정의한다

package my.was.mywas.commons;

public interface Constants {
  static final String AUTH = "auth";
  static final String AUTHORIZATION = "Authorization";
  static final String BEARER = "Bearer";
  static final String CONTENT_DISPOSITION = "Content-disposition";
  static final String CONTENT_TYPE = "Content-type";
  static final String CTYPE_FILE = " application/octet-stream";
  static final String CTYPE_FORM = "application/x-www-form-urlencoded";
  static final String CTYPE_HTML = "text/html";
  static final String CTYPE_JSON = "application/json";
  static final String CTYPE_MULTIPART = "multipart/form-data";
  static final String CTYPE_TEXT = "text/plain";
  static final String EXTRA_INFO = "extraInfo";
  static final String PROF_DEV = "dev";
  static final String PROF_LOCAL = "local";
  static final String PROF_MY = "my";
  static final String PROF_PROD = "prod";
  static final String REFERER = "Referer";
  static final String ROLE_ADMIN = "ROLE_ADMIN";
  static final String ROLE_USER = "ROLE_USER";
  static final String STATIC_ACCESS = "static-access";
  static final String TOK_TYP_ACC = "ACC";
  static final String TOK_TYP_REF = "REF";
  static final String X_FORWARDED_FOR = "X-Forwarded-For";
  static final String KOKR = "ko-KR";
  static final String RESCD_OK = "0000";
  static final String RESCD_FAIL = "9999";
  static final String NOT_PERMITTED_USER = "NOT PERMITTED USER";
  static final String ISO88591 = "ISO8859-1";
}

3-2. 공통Exception (ApiException.java)

  • 공통 Exception 을 작성한다

package my.was.mywas.commons;

import static com.ntiple.commons.ConvertUtil.cat;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter @ToString @Builder
public class ApiException extends RuntimeException {
  public HttpStatus status;
  public Integer errcd;
  public String errmsg;

  public ApiException(Integer errcd, String errmsg) {
    super(cat("[", errcd, "]", errmsg, ""));
    this.status = HttpStatus.INTERNAL_SERVER_ERROR;
    this.errcd = errcd;
    this.errmsg = errmsg;
  }

  public ApiException(Integer errcd, String errmsg, HttpStatus status) {
    super(cat("[", errcd, "]", errmsg, "/", status));
    this.status = status;
    this.errcd = errcd;
    this.errmsg = errmsg;
  }
}

3-3. 공통설정 (SystemSettings.java)

  • 공통 설정 UTIL 을 작성한다 (암호화 키 등을 공통에서 사용.)

package my.was.mywas.commons;

import static com.ntiple.commons.ConvertUtil.cast;

import java.util.LinkedHashMap;
import java.util.Map;

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

import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j @Component @Getter @Setter
public class SystemSettings implements ApplicationContextAware {

  private static SystemSettings instance;

  private boolean alive;

  private Map<String, Object> gprops;

  private ApplicationContext applicationContext;

  @Value("${spring.profiles.active:local}") private String profile; 
  /** aes 암호화 키 */
  @Value("${security.key.aes.secret:}") private String keySecret;
  /** rsa 암호화 개인키 */
  @Value("${security.key.rsa.privatekey:}") private String keyPrivate;
  /** rsa 암호화 공개키 */
  @Value("${security.key.rsa.publickey:}") private String keyPublic;
  /** access token security */
  @Value("${security.jwt.access.secret:}") private String secretAcc;
  /** refresh token security */
  @Value("${security.jwt.refresh.secret:}") private String secretRef;
  /** access token expiry */
  @Value("${security.jwt.access.expire:0}") private Long exprAcc;
  /** refresh token expiry */
  @Value("${security.jwt.refresh.expire:0}") private Long exprRef;

  @PostConstruct public void init() {
    log.trace("INIT:{}", SystemSettings.class);
    instance = this;
    alive = true;
    gprops = new LinkedHashMap<>();
  }

  public static final SystemSettings getInstance() {
    if (instance == null) {
      instance = new SystemSettings();
      instance.init();
    }
    return instance;
  }

  @Override @SuppressWarnings("null") 
  public void setApplicationContext(ApplicationContext appctx) throws BeansException {
    this.applicationContext = appctx;
  }

  public static final boolean containsPropertyKey(String key) {
    return instance.gprops.containsKey(key);
  }

  public static final String getProperty(String key) {
    return cast(instance.gprops.get(key), "");
  }

  public static final void setProperty(String key, String val) {
    instance.gprops.put(key, val);
  }
}

3-4. 공통DTO Entity (CommonEntity.java)

  • 공통적으로 사용할 DTO Entity 를 정의한다 (검색등에서 공통으로 사용)

package my.was.mywas.commons;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Date;
import java.util.List;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CommonEntity {

  @Target({METHOD, FIELD, TYPE})
  @Retention(RetentionPolicy.RUNTIME) 
  public static @interface SecureOut { }

  @Target({METHOD, FIELD, TYPE})
  @Retention(RetentionPolicy.RUNTIME) 
  public static @interface IgnoreOut { }

  @Schema(title = "로그인 요청 파라메터 (Login)")
  @AllArgsConstructor
  @NoArgsConstructor
  @Getter @Setter @ToString @Builder
  public static class Login {
    @Schema(title = "사용자ID")
    private String userId;
    @Schema(title = "비밀번호")
    private String passwd;
  }

  @AllArgsConstructor @NoArgsConstructor
  @Getter @Setter @ToString @Builder
  public static class AuthInfo {
    private String ipAddr;
    private String userNm;
    private Boolean isAdmin;
  }

  @Schema(title = "로그인 응답 타입 (AuthResult)")
  @AllArgsConstructor
  @NoArgsConstructor
  @Getter @Setter @ToString @Builder @SecureOut
  public static class AuthResult {
    @Schema(title = "사용자 ID", hidden = true)
    @SecureOut private String userId;
    @Schema(title = "사용자 이름", hidden = true)
    @SecureOut private String userNm;
    @Schema(title = "응답코드")
    private String rescd;
    @Schema(title = "응답타입")
    private String restyp;
    @Schema(title = "액세스 토큰", hidden = true)
    @SecureOut private String accessToken;
    @Schema(title = "리프레시 토큰", hidden = true)
    @SecureOut private String refreshToken;
  }

  @Schema(title = "검색결과 (SearchEntity)")
  @AllArgsConstructor
  @NoArgsConstructor
  @Getter @Setter @ToString @Builder
  public static class SearchEntity<T> {
    @Schema(title = "검색타입")
    private String searchType;
    @Schema(title = "검색키워드")
    private String keyword;
    @Schema(title = "검색시작")
    private Integer rowStart;
    @Schema(title = "검색수량")
    private Integer rowCount;
    @Schema(title = "한화면에 표기할 페이지갯수")
    private Integer pagePerScreen;
    @Schema(title = "정렬타입")
    private String orderType;
    @Schema(title = "결과리스트총갯수")
    private int rowTotal;
    @NotBlank @Schema(title = "결과리스트")
    private List<T> list;

    public Pageable getPageable() {
      int page = 0;
      int rowStart = this.rowStart;
      int rowCount = this.rowCount;
      if (rowStart > 0 && rowCount > 0) { page = rowStart / rowCount; }
      if (rowCount < 1) { rowCount = 1; }
      if (page < 1) { page = 0; }
      log.debug("PAGE:{} / COUNT:{} / START:{} / COUNT:{}", page, rowCount, this.rowStart, this.rowCount);
      Pageable ret = PageRequest.of(page, rowCount);
      return ret;
    }
  }

  @Schema(title = "공통 REST 결과타입 (Result)")
  @AllArgsConstructor
  @NoArgsConstructor
  @Getter @Setter @ToString @Builder
  public static class Result {
    @NotBlank @Size(min = 1, max = 20)
    @Schema(title = "결과코드")
    private String rescd;
    @Schema(title = "오류코드")
    private String errcd;
    @Schema(title = "오류메시지")
    private String msg;
    @Schema(title = "결과데이터")
    private Object data;
  }

  @Schema(title = "공통 환경변수 결과타입")
  @AllArgsConstructor
  @NoArgsConstructor
  @Getter @Setter @ToString @Builder
  public static class InitObj {
    @Schema(title = "서버현재시간 (Unix-Time / 시간동기화 용)")
    private Date current;
    @Schema(title = "지원언어 (KO_KR / EN_US)")
    private String locale;
    @Schema(title = "서버 인코딩(UTF-8)")
    private String encoding;
    @Schema(title = "액세스토큰 만료기간")
    private Long expirecon;
    @Schema(title = "암호동기화용 토큰")
    private String check;
  }

  @Getter @Setter @ToString
  public static class AuthDetail {
    private AuthExtra extraInfo;
  }

  @Getter @Setter @ToString
  public static class AuthExtra {
    private String ipAddr;
    private String userNm;
  }
}

3-5. 공통 WEB-UTIL (WebUtils.java)

  • 공통으로 사용할 WEB-UTIL 을 작성한다 (원격지IP 등 request 분석정보, response 가공 등의 역할)

package my.was.mywas.commons;

import static com.ntiple.commons.ConvertUtil.capitalize;
import static com.ntiple.commons.ConvertUtil.cast;
import static com.ntiple.commons.ConvertUtil.cat;
import static com.ntiple.commons.ConvertUtil.EMPTY_CLS;
import static com.ntiple.commons.ConvertUtil.EMPTY_OBJ;
import static com.ntiple.commons.ConvertUtil.isPrimeType;
import static my.was.mywas.commons.Constants.AUTHORIZATION;
import static my.was.mywas.commons.Constants.BEARER;
import static my.was.mywas.commons.Constants.REFERER;
import static my.was.mywas.commons.Constants.X_FORWARDED_FOR;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.SecureOut;

@Slf4j
public class WebUtils {
  static final String PTN_SCHEM_HTTP = "^http[s]{0,1}[:][/][/]";

  /** 토큰정보 읽어오기 */
  public static String getAuthToken() { return getAuthToken(curRequest()); }
  public static String getAuthToken(HttpServletRequest req) {
    String hval = req.getHeader(AUTHORIZATION);
    log.trace("AUTH-HEADER:{}", hval);
    if (hval != null && hval.startsWith(BEARER) && hval.length() > BEARER.length() + 2) {
      return hval.substring(BEARER.length() + 1);
    }
    return null;
  }

  /** 현재 Request 객체 읽어오기 */
  public static HttpServletRequest curRequest() {
    return (cast(RequestContextHolder.getRequestAttributes(), ServletRequestAttributes.class))
      .getRequest();
  }

  /** 현재 Response 객체 읽어오기 (SecurityConfig.AuthFilter 에서 저장된 Response) */
  public static HttpServletResponse curResponse() { return curResponse(curRequest()); }
  public static HttpServletResponse curResponse(HttpServletRequest request) {
    if (request != null) {
      try {
        Object obj = request.getAttribute(HttpServletResponse.class.getName());
        if (obj != null) {
          if (obj instanceof HttpServletResponse) {
            return cast(obj, HttpServletResponse.class);
          }
        }
      } catch (Exception ignore) { }
    }
    return null;
  }

  /** 원격지 IP주소 읽어오기 (앞단에 웹서버Proxy 가 있을수 있으므로 X-Forward-For 헤더까지 읽어본다) */
  public static String remoteAddr() { return remoteAddr(curRequest()); }
  public static String remoteAddr(HttpServletRequest req) {
    String ret = null;
    ret = req.getHeader(X_FORWARDED_FOR);
    if (ret == null) { ret = req.getHeader("Proxy-Client-IP"); }
    if (ret == null) { ret = req.getHeader("WL-Proxy-Client-IP"); }
    if (ret == null) { ret = req.getHeader("HTTP_CLIENT_IP"); }
    if (ret == null) { ret = req.getHeader("HTTP_X_FORWARDED_FOR"); }
    if (ret == null) { ret = req.getRemoteAddr(); }
    return ret;
  }

  /** 호출지 (Referer) 읽어오기 */
  public static String referer() { return referer(curRequest()); }
  public static String referer(HttpServletRequest req) {
    String ret = null;
    ret = req.getHeader(REFERER);
    return ret;
  }

  /** Requet URL 에서 URI 분리 */
  public static String getUri(String urlStr, List<String> hostNames) {
    String ret = "";
    if (urlStr == null || "".equals(urlStr)) { return ret; }
    urlStr = urlStr.trim().replaceAll(PTN_SCHEM_HTTP, "").trim();
    if (hostNames != null) {
      LOOP:
      for (String hostName : hostNames) {
        if (urlStr.startsWith(hostName)) {
          ret = urlStr.substring(hostName.length());
          break LOOP;
        }
      }
    }
    return ret;
  }

  /** @SecureOut 으로 어노테이션 된 필드는 RestResponse 에서 삭제되도록 처리 */
  public static <T> T secureOut(T prm) {
    if (prm == null) { return prm; }
    T ret = prm;
    Class<?> cls = prm.getClass();
    if (isPrimeType(cls)) { return prm; }
    log.trace("CHECK1:{} : {}", cls.getSimpleName(), cls.isAnnotationPresent(SecureOut.class));
    if (cls.isAnnotationPresent(SecureOut.class)) {
      Field[] fields = cls.getDeclaredFields();
      for (Field field : fields) {
        log.trace("CHECK2:{}.{} : {}", cls.getSimpleName(), field.getName(), field.isAnnotationPresent(SecureOut.class));
        if (field.isAnnotationPresent(SecureOut.class)) {
          try {
            Method setter = cls.getDeclaredMethod(cat("set", capitalize(field.getName())), field.getType());
            if (setter != null) {
              setter.invoke(prm, new Object[]{ null });
            }
          } catch (Exception ignore) { }
        } else {
          Object val = null;
            Method mtd = null;
            try {
              mtd = cls.getMethod(cat("get", capitalize(field.getName())), EMPTY_CLS);
              if (mtd != null) { val = mtd.invoke(prm, EMPTY_OBJ); }
            } catch (Exception e) {
              log.debug("E:{}", e);
            }
          if (val != null) {
            if (val instanceof List) {
              List<?> list = cast(val, list = null);
              for (Object itm : list) {
                secureOut(itm);
              }
            } else if (val instanceof Map) {
              Map<String, Object> map = cast(val, map = null);
              for (String key : map.keySet()) {
                secureOut(map.get(key));
              }
            } else {
              secureOut(val);
            }
          }
        }
      }
    }
    return ret;
  }
}

3-6. 공통 Response (RestResponse.java)

  • REST-RESPONSE 를 간편하게 사용할 수 있도록 해 주는 유틸을 작성한다

package my.was.mywas.commons;

import static com.ntiple.commons.ConvertUtil.cast;
import static com.ntiple.commons.ConvertUtil.cat;
import static my.was.mywas.commons.Constants.AUTHORIZATION;
import static my.was.mywas.commons.Constants.BEARER;
import static my.was.mywas.commons.Constants.CONTENT_TYPE;
import static my.was.mywas.commons.Constants.CTYPE_HTML;
import static my.was.mywas.commons.WebUtils.curRequest;
import static my.was.mywas.commons.WebUtils.remoteAddr;
import static my.was.mywas.commons.WebUtils.secureOut;

import java.util.Map;
import java.util.concurrent.Callable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

import com.ntiple.commons.CryptoUtil.AES;

import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import my.was.mywas.commons.CommonEntity.AuthResult;
import my.was.mywas.commons.CommonEntity.InitObj;

@Slf4j @Component
public class RestResponse {
  private static RestResponse instance;

  @Autowired SystemSettings settings;

  @PostConstruct public void init() {
    log.trace("INIT:{}", RestResponse.class);
    instance = this;
  }

  public static <T> ResponseEntity<T> response(Callable<T> exec) {
    HttpHeaders hdrs = new HttpHeaders();
    HttpStatus status = HttpStatus.OK;
    SystemSettings settings = instance.settings;
    Object res = null;
    try {
      HttpServletRequest req = curRequest();
      String ipaddr = remoteAddr(req);
      String uri = req.getRequestURI();
      log.trace("IP:{} / URI:{} / IS-ADMIN:{} / USER-ID:{} / EXTRA:{}", ipaddr, uri);
      res = exec.call();
      log.trace("RES:{}", res);
      if (res instanceof String) {
        /** 문자열 결과인 경우 바로 리턴한다. */
        hdrs.add(CONTENT_TYPE, CTYPE_HTML);
        ResponseEntity<T> ret = cast(
          new ResponseEntity<>(res, hdrs, status), ret = null);
        return ret;
      } else if (res instanceof AuthResult) {
        AuthResult ares = cast(res, ares = null);
        Long exptAcc = settings.getExprAcc();
        Long exptRef = settings.getExprRef();
        String encstr = "";
        log.debug("ARES:{}", ares);
        if (ares.getAccessToken() != null) {
          if (ares.getRefreshToken() != null) {
            try {
              /** 응답헤더에 사용자ID, 토큰정보등을 암호화 하여 내려보낸다. (공백구분) */
              encstr = AES.encrypt(settings.getKeySecret(), cat(
                ares.getUserId(), " ", ares.getUserNm(), " ",
                ares.getAccessToken(), " ", ares.getRefreshToken(), " ",
                exptAcc, " ", exptRef, " " 
              ));
            } catch (Exception e) {
              log.debug("E:{}", e.getMessage());
            }
            hdrs.add(AUTHORIZATION, cat(BEARER, " ", encstr));
          } else {
            try {
              /** 토큰리프레시 인 경우 액세스토큰만 발급해서 내려보낸다 */
              encstr = AES.encrypt(settings.getKeySecret(), cat(
                ares.getUserId(), " ", ares.getUserNm(), " ",
                ares.getAccessToken(), " ", exptAcc)
              );
            } catch (Exception e) {
              log.debug("E:{}", e.getMessage());
            }
            hdrs.add(AUTHORIZATION, cat(BEARER, " ", encstr));
          }
        }
      } else if (res instanceof InitObj) {
        InitObj ires = cast(res, ires = null);
        log.trace("CHECK:{}", ires.getCheck());
      }
    } catch (Exception e) {
      if (e instanceof ApiException) {
        // log.debug("ERROR:{}{}{}", e.getMessage(), C.CRNL, errstack(e));
        ApiException ee = cast(e, ApiException.class);
        if (ee.status != null) {
          status = ee.status;
        } else {
          status = HttpStatus.INTERNAL_SERVER_ERROR;
        }
        res = Map.of("message", ee.errmsg);
      } else {
        log.debug("ERROR:", e);
        status = HttpStatus.INTERNAL_SERVER_ERROR;
      }
    }
    ResponseEntity<T> ret = cast(new ResponseEntity<>(secureOut(res), hdrs, status), ret = null);
    log.debug("RESULT-OK:{}", hdrs);
    return ret;
  }

  public static void log(String format, Object... args) {
    log.debug(format, args);
  }
}

3-7. JWT 토큰 발급기 (TokenProvider.java)

  • JWT 토큰 발급기를 작성한다 (추후 SecurityConfig.java 에서 호출)

package my.was.mywas.commons;

import static com.ntiple.commons.ConvertUtil.cast;
import static com.ntiple.commons.ConvertUtil.cat;
import static com.ntiple.commons.ConvertUtil.EMPTY_OBJ;
import static my.was.mywas.commons.Constants.AUTH;
import static my.was.mywas.commons.Constants.EXTRA_INFO;
import static my.was.mywas.commons.Constants.TOK_TYP_REF;

... 중략 ...

@Slf4j @Component
public class TokenProvider {
  @Autowired SystemSettings settings;
  /** 엑세스토큰 */
  Key keyAcc;
  /** 리프레시토큰 */
  Key keyRef;
  @PostConstruct public void init () {
    this.keyAcc = Keys.hmacShaKeyFor(Decoders.BASE64.decode(settings.getSecretAcc()));
    this.keyRef = Keys.hmacShaKeyFor(Decoders.BASE64.decode(settings.getSecretRef()));
  }

  /** 토큰생성 (토큰타입이 액세스/리프레시 인지 여부에 따라 다른 KEY 를 사용한다) */
  public String createToken(String tokenType, Authentication auth, Object extraInfo) throws Exception {
    /** Authentication -> Token */
    Key key = keyAcc;
    long expire = settings.getExprAcc();
    switch (tokenType) {
    case TOK_TYP_REF: {
      key = keyRef;
      expire = settings.getExprRef();
    } break;
    default: break;
    }
    return createToken(key, expire, auth, extraInfo);
  }

  public String createToken(Key key, long expire, Authentication auth, Object extraInfo) throws Exception {
    String authorities = auth.getAuthorities().stream()
      .map(GrantedAuthority::getAuthority)
      .collect(Collectors.joining(","));
    return Jwts.builder()
      .setSubject(auth.getName())
      .claim(AUTH, authorities)
      .claim(EXTRA_INFO, extraInfo)
      .signWith(key, SignatureAlgorithm.HS512)
      .setExpiration(new Date(System.currentTimeMillis() + expire))
      .compact();
  }

  /** 인증정보읽어오기 */
  public Authentication getAuth(String tokenType, String token, HttpServletRequest req) {
    /** Token -> Authentication */
    Claims claims = null;
    claims = parseToken(tokenType, token, req);
    log.debug("EXPIRE-LEFT:{}s", (claims.getExpiration().getTime() - System.currentTimeMillis()) / 1000);
    return getAuth(claims);
  }

  public Authentication getAuth(Claims claims) {
    Collection<SimpleGrantedAuthority> authorities = null;
    User principal = null;
    if (claims != null) {
      /**
       * 디비를 거치지 않고 토큰에서 값을 꺼내 바로 시큐리티 유저 객체를
       * 만들어 Authentication을 만들어 반환하기에 유저네임, 권한 외 정보는 알 수 없다.
       */
      authorities =
        Arrays.stream(claims.get(AUTH).toString().split(","))
          .map(SimpleGrantedAuthority::new)
          .collect(Collectors.toList());
      principal = new User(claims.getSubject(), "", authorities);
    } else {
      principal = new User(null, "", null);
    }
    UsernamePasswordAuthenticationToken ret =
      new UsernamePasswordAuthenticationToken(principal, "", authorities);
    ret.setDetails(claims);
    return ret;
  }

  /** 토큰 복호화 분석 */
  public Claims parseToken(String tokenType, String token, HttpServletRequest req) {
    Claims ret = null;
    Object o = null;
    String att = cat(tokenType, token);
    if ((o = req.getAttribute(att)) == null) {
      try {
        Key key = keyAcc;
        if (tokenType == TOK_TYP_REF) { key = keyRef; }
        JwtParser parser = Jwts.parserBuilder().setSigningKey(key).build();
        Jws<Claims> jws = parser.parseClaimsJws(token);
        ret = jws.getBody();
        req.setAttribute(att, ret);
      } catch (Exception e) {
        /** 토큰리프레시의 경우 토큰타입이 다르기때문에 항상 실패함 */
        log.trace("FAIL TO PARSE:{} / {} / {}", e.getMessage(), tokenType, token);
        req.setAttribute(att, EMPTY_OBJ);
      }
      log.trace("JWT-PARSE:{}", ret);
    } else if (o != EMPTY_OBJ) {
      ret = cast(o, ret);
    }
    return ret;
  }
}

  • 지금까지 추가된 공통 파일들이다

src/main/java/commons [dir]
├── ApiException.java (공통 Exception)
├── CommonEntity.java (공통 DTO Entity)
├── Constants.java (공통 상수)
├── RestResponse.java (공통 RestResponse 유틸)
├── SystemSettings.java (공통 설정 유틸)
├── TokenProvider.java (토큰발급기)
└── WebUtils.java (공통 WEB-UTIL)

  • 이제 작성된 공통유틸을 사용해 아래와 같이 코딩이 가능하다 (아래는 컨트롤러 예시)

... 중략 ...
import static kr.kcdf.commons.RestResponse.response;
... 중략 ...
@Slf4j @RestController
@RequestMapping("/api/user")
public class Cmn01001Controller {
  static final String CONTROLLER_TAG = "사용자 API";
  ... 중략 ...
  /** OPEN-API 용 어노테이션 */
  @Operation(summary = "사용자검색", tags = { CONTROLLER_TAG })
  @PostMapping("/searchUser")
  public ResponseEntity<List<User>> searchUser(SearchEntity<?> prm) {
    /**
     * RestResponse.response 를 사용시
     * - 오류가 발생하는 경우 자동으로 오류처리 하여 메시지를 출력한다.
     * - 리턴값이 @SecureOut 이 지정되어있고  @SecureOut 으로 지정된 필드가 있다면 삭제되어 출력된다 (개인정보 출력제한 등)
     **/
    return response(() -> service.findCode(prm));
  }
}

4. 이번편 마무리 및 레포트 분량 재분배.....

이번편에 비즈니스로직까지 완료하려 했으나 분량조절 실패다....

(스터디때도 분량조절 실패하더만. 여전히..... 쿨럭..!)

최초 목적은 정말 가볍게 정리할 생각이었는데....

아무래도 욕심이 과했던 듯 하다.

그래서 백엔드 레포트2편 정도로,
프론트엔드 레포트3편 정도로 분량을 재분배 할 계획이다.

고로 다음편은 비즈니스 로직 정리하고 백엔드 (WAS) 마무리 예정이다.

결과물 git주소 : https://gitlab.ntiple.com/developers/study-202403

github : https://github.com/lupfeliz/study-202403

[ ←전편보기 / 1편, 3편, 4편, 5편, 6편 / 다음편보기→ ]

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다