Intro


과연 할 수 있을까?

파이널 프로젝트를 시작하면서 기획이나 설계 단계에서 이 정도는 할 수 있겠지 하는 마음으로 욕심을 한껏 부려서 커트라인을 높이게 되었다. 그러다보니 생각보다 개발하고자 하는 범위도 늘어나고 고려해야 할 사항들도 많아졌다.

팀원들과 일일 스프린트를 진행하면서 물론 아이디어를 제안하고 적용하는 것도 중요하지만 짧은 개발일정 내에 산출물을 만들어야 하기 때문에 구현 가능성을 무시할 수는 없어서 중간중간 가지치기를 요청했다.

프로젝트를 통해서 하고자 하는 것과는 방향성이 다른 기능들은 미련없이 쳐낼 수 있었지만 배운대로만 적용해보고 끝내고 싶지는 않아서 필요한 기능들은 그대로 두고 개발을 시작하기로 하였다.

사실, 기획 및 설계 단계에서 내놓은 다양한 기능들에 대해 구현 가능성을 일일이 따져보고 일정을 산출하고 프로잭트의 개발 범위를 결정하는 것이 참으로 어려웠다.

그래서 프로젝트의 개발 일정을 모두 최악의 상황을 염두에 두고 정하게 되었다. 생각보다 타이트한 일정이지만 정해진 일정대로 프로젝트를 개발하기 위해서 일 단위로 개발내역 회고를 한다던가, 문제 발생시 고민시간 제한을 두는 등의 팀 단위 스프린트를 진행하면서 효율적인 개발을 할 수 있을 것이다. 그리고 해야만 한다.




Week 20

카카오 클라우드 스쿨 20주차 91~95일까지의 공부하고 고민했던 흔적들을 기록하였습니다.

Spring에서의 유효성 검사는 어디서 해야하는 걸까?

요즘 사용자 입력을 받아야 하는 기능들을 개발하면서 유효성 검사를 어떻게 어디서 해야할지 고민이 많이 하게 되었다. 여기서 어디서 해야할지라는 의문은 서버에서 처리해야 할지, 아니면 하던대로 클라이언트에서 처리 할지 고민이 된다는 것이다.

유효성을 검사한다는 것은 어플리케이션에서 사용자가 입력한 데이터가 유효한지 검사하는 것을 뜻한다. 그래서 유효성 검사를 적절하게 수행한다면 서버 애플리케이션에서 NullPointerException이나 예상하지 못한 오류를 사전에 방지할 수 있다.


클라이언트와 서버에서의 유효성 검사

클라이언트와 서버 단에서 유효성 검사를 어느 시점에 할 수 있을까? 클라이언트에서 서버로 데이터를 전송하기 전서버가 클라이언트에서 요청을 받은 후 로 나눌 수 있다.

클라이언트에서 서버로 전송하기 전

클라이언트에서 유효성을 검사할 경우에는 사용자가 입력하는 도중(changed 이벤트)에 검사를 하여 보여주기도 하고, 사용자가 데이터 입력을 완료한 후 전송하기 직전(submit 이벤트)에 진행할 수 있다.

클라이언트 단에서 진행하는 것은 일반적으로 여러 상황에서 두루 사용한다. 데이터를 서버로 전송하지 않고 결과를 바로 확인할 수 있으며 트래픽을 줄일 수 있지만, 보안성 측면에서 이슈가 발생할 가능성이 있다.

서버가 클라이언트로 요청을 받은 후

클라이언트에서 입력 데이터를 받은 서버에서 유효성 검사를 한다면 Controller, Service, Repository Layer에서 할 수 있다.

특별한 경우가 아니면 입력 데이터는 보통 Controller에서 유효성 검사를 진행하고, 조회할 데이터의 유효성 검사는 Service에서 수행하게 된다. 이 때, Service에서 데이터의 중복 및 존재 여부를 확인하는 것은 필수적으로 진행해야 한다.

서버에서 유효성 검사를 진행한다면 보안적인 측면에서는 클라이언트에서 유효성 검사를 하는것보다 뛰어나지만 결과를 다시 전송해야 하기 때문에 트래픽이 증가하게 된다.

그럼에도 클라이언트에서 유효성 검사를 하더라도 서버에서 다시 수행하는 것이 바람직하다.


Spring에서 유효성 검사를 진행하는 방법은?

Spring에서 입력값에 대한 유효성 검사를 하기 위한 방법에는 JSR-303 Spec에서 제공하는 어노테이션을 이용하는 방법과 Validation 인터페이스를 구현하는 방법 등이 있다.

이번 글에서는 JSR-303을 다뤄보고 실습해보겠다.

JSR-303이란?

JSR-303이란 자바 기반의 Bean Validation 스펙으로, Java EE 6 버전부터 포함되었다. 이 스펙은 Bean 유효성 검사에 대한 표준화된 방법을 제공한다.

유효섬 검사 규칙은 어노테이션을 사용하여 작성할 수 있으며, Spring에서는 @Validated 어노테이션을 사용하여 유효성 검사가 필요한 메소드나 클래스에 적용하면 된다.

JSR-303의 주요 기능

  • 어노테이션 기반의 유효성 검사
    JSR-303은 어노테이션을 사용하여 Bean의 필드나 메서드 등에 대한 유효성 검사 규칙을 정의할 수 있다.

  • 메시지 다국어 처리
    JSR-303은 다국어 처리를 위한 메시지 번들(Message Bundle)을 지원하여 이를 통해 유효성 검사 실패 시 출력되는 메시지를 다국어로 처리할 수 있다.

  • 유효성 검사 그룹화
    JSR-303은 유효성 검사 그룹화를 지원한다.. 유효성 검사 그룹화를 통해 특정 시나리오에서만 적용되는 유효성 검사 규칙을 정의할 수 있다.

이와 같이 Spring에서는 JSR-303의 어노테이션을 사용하여 Bean의 유효성 검사를 수행할 수 있다. 이를 통해 개발자는 유효성 검사 규칙을 명시적으로 작성하지 않고도 간단하게 유효성 검사를 구현할 수 있게 된다.

@Valid 어노테이션

@Vaild 어노테이션은 앞서 살펴본 JSR-303에 등장한 자바 표준 스팩의 검증 애노테이션이다.

javax.validation 패키지에 속해 있으며, ValidationAutoConfigurationLocalValidatorFactoryBean 메서드를 통해 Bean으로 등록된다. ArgumentResolver 단계에서 Controller에 넘어가기전 검증을 진행한다. ArgumentResolver 단계에서 동작하기 때문에, 기본적으로 컨트롤러에서만 동작하며, Service, Repository 같이 다른 계층에서는 유효성 검사를 할 수 없다.

@Validated 어노테이션

@Vaild 어노테이션과는 달리 Spring에서 제공하는 유효성 검사 애노테이션이다. 또한, @Validated@Valid의 기능을 포함하며, 유효성 검사에 대한 그룹을 지정할 수 있다.

org.springframework.validation.annotation 패키지에 속해 있으며, ValidationAutoConfigurationMethodValidationPostProcessor 메서드를 통해 Bean으로 등록된다.

AOP 기반으로 동작하며, 각 메서드의 요청을 가로채서 검증을 진행한다. AOP 기반으로 동작하기 때문에 Spring Bean으로 등록된 모든 계층에서 유효성 검사를 진행할 수 있다. 하지만, 검증과정은 Controller에서 끝내는 것이 좋다.

@Valid@Validated
제공자바 표준 스팩 JSR-303Spring Framework
동작 방식ArgumentResolverAOP 기반
사용가능 계층ControllerSpring Bean으로 등록된 모든 계층
예외MethodArgumentNotValidExceptionConstraintViolationException

유효성 검사에 사용할 수 있는 어노테이션 종류는?

JSR 표준 스펙은 다양한 검증 애노테이션을 제공하고 있으며 그 중 많이 사용되는 어노테이션은 다음과 같다.

어노테이션설명
@NotNull해당 값이 null이 아닌지 검증
@NotEmpty해당 값이 null, "" 이 아닌지 검증
@NotBlank해당 값이 null, "", " " 이 아닌지 검증
@AssertTrue해당 값이 true 인지 검증
@AssertFalse해당 값이 false 인지 검증
@Size(int min, int max)해당 값의 크기가 min ~ max 인지 검증
String, Collection, Map, Array에 적용 가능
@Min(long value)해당 값이 value보다 작지 않은지 검증
@Max(long value)해당 값이 value보다 크지 않은지 검증
@Pattern(String regexp)해당 값이 해당 정규식을 지키고 있는지 검증
@Email해당 값이 Email 형식을 지키고 있는지 검증

이외에도 Hibernate에서 제공하는 추가 어노테이션들이 있는데 공식 문서에서 참고하면 된다.


Spring에서 JSR-303으로 유효성 검사하기

자, 앞에서 Spring에서 유효성 검사를 진행할 수 있는 방법과 어노테이션에 대해서 알아볼 수 있었다. 이번 글에서는 JSR-303에서 제공하는 어노테이션을 활용해 간단히 유효성 검사를 진행하는 실습을 진행하려 한다.

이 때, 유효성 검사는 각 계층에서 데이터가 넘어오는 시점에 수행해야 하기 때문에 DTO 클래스에서 수행하였다.

Spring Boot 프로젝트 생성하기

Spring Boot 3.0.4 버전에서 java 17을 선택했다. 그리고 추가한 의존성들은 다음과 같다.

  • devtools, lombok, spring web, Spring Data JPA, mariaDB, Validation

DTO 및 Controller 클래스 작성하기

dto.ValidationRequestDTO.java

import jakarta.validation.constraints.*;
import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDTO {
    @NotBlank
    private String name;

    @Email
    private String email;

    @Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
    private String phoneNumber;

    @Min(value = 20)
    @Max(value = 40)
    private int age;

    @Size(min = 0, max = 40)
    private String description;

    @Positive
    private int count;

    @AssertTrue
    private boolean booleanCheck;
}

controller.ValidationController.java

import com.kakao.lango.validation.dto.ValidRequestDTO;
import jakarta.validation.Valid;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RequestMapping("/validation")
@RestController
public class ValidationController {
    @PostMapping("/valid")
    public ResponseEntity<String> checkValidationByValid(@Valid @RequestBody ValidRequestDTO validRequestDTO) {
        log.info(validRequestDTO.toString());
        return ResponseEntity.status(HttpStatus.OK).body(validRequestDTO.toString());
    }
}

/validation/vaild로 POST 요청을 보낼 때 유효성 검사를 진행하기 위한 Controller 클래스를 작성하였으며, 실제로 요청으로 들어온 입력 값을 검증하기 위한 DTO 클래스를 작성하였다.

public ResponseEntity<String> checkValidationByValid(@Valid @RequestBody ValidRequestDTO validRequestDTO) {
	...
}

이제 포스트맨으로 테스트를 해보자.

정상적인 요청값을 넘겨준다면 ValidRequestDTO에 값을 잘 담아오는 것을 확인할 수 있었다.

그런데, 이메일 형식을 잘못 작성했다면 400에러를 출력해주는 것을 알 수 있다.

동일하게, 핸드폰 번호 양식을 잘못 작성해도 400에러를 출력한다.

이 때, 컨트롤러 클래스에 유효섬 검사가 필요한 메소드에 꼭 @Vaild 어노테이션을 붙여야 유효섬 검사를 진행하게 됨을 유의해야 한다.

만약 @Vaild 어노테이션을 깜빡하고 작성하지 않았다면 위와 같이 잘못된 입력값을 그대로 DTO에 담아버리니 항상 주의하자.


유효성 검사에는 어떤 방식이 사용해야 할까?

Spring에서 유효성 검사를 진행할 때는 Validator 인터페이스 구현하는 것보다 어노테이션 기반의 JSR-303 방식으로 진행하는 것이 더 편리하고 유지보수하기 쉽다고 생각한다.

어노테이션 기반의 JSR-303을 사용하면, 유효성 검사를 하기 위한 규칙을 어노테이션으로 쉽게 정의할 수 있으며, 해당 어노테이션을 사용하여 객체나 필드에 대한 유효성 검사를 수행할 수 있게 된다. 또한 Spring에서는 어노테이션 기반의 유효성 검사를 위한 다양한 기능을 제공하고 있다.

그에 반해, Validator 인터페이스를 구현하는 방법은 어노테이션 기반보다 더 유연하게 검사 규칙을 정의할 수 있으며, 검사 규칙이 동적으로 변경되어야 하는 경우에는 Validator 인터페이스를 구현하는 방법이 더 적절한 솔루션이 될 수 있다.

따라서, 유효성 검사 규칙이 단순하고 변경될 가능성이 적다면 어노테이션 기반의 JSR-303 방식을 사용하는 것이 개발자가 유지보수하는데 더 쉬울 것이고, 더 복잡한 유효성 검사 규칙을 만들어서 사용해야 한다면 Validatior 인터페이스를 구현해서 사용해야 하는 것이 좋을 것으로 생각된다.



API 통신시 예외가 발생한다면? RestTemplate 2번째 이야기

이전 회고록에서 RestTemplate에 대해서 한번 다루어 보았는데, 놓친 부분이 있다는 것을 알게 되었다. 바로 RestTemplate을 통해 외부 API로 통신할 때 예기치 못한 문제가 발생할 수 있다는 것이다.

기존의 RestTemplate 설정 파일의 문제

이전에 만들었던 RestTemplateConfig 설정 파일에서는 단순히 RestTemplate의 인스턴스를 만들어서 스프링 빈으로 등록헤두는 것 이외로 별다른 설정을 해두지 않았었다.

config.RestTemplateConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

RestTemplate을 스프링 빈으로 등록해두었으니 비즈니스 로직에서 일일이 RestTemplate의 인스턴스를 만들지 않아도 되지만, 이렇게만 설정하면 RestTemplate 인스턴스가 만들어진 후 어떠한 설정도 적용되지 않다는 것을 알게 되었다.

RestTemplate Bean 타임아웃 설정 추가하기

그렇다면 RestTemplate의 인스턴스를 만들고 어떤 설정을 추가로 해야할까? 기본적으로는 Connection Timeout 설정을 우선적으로 해야한다.

RestTemplate은 기본적으로 무제한 대기 시간을 가지고 있다고 한다. 이 말은 외부 API를 호출하여 통신하는데 응답이 올 때까지 계속 기다리고 있다는 말이다. 이를 방지하기 위해서는 Connection Timeout을 설정하여 일정 시간 이상 서버와 연결이 이루어지지 않으면 TimeoutException을 발생시키도록 해야한다.

 @Bean
 public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
 	return restTemplateBuilder
    		.setConnectTimeout(Duration.ofSeconds(5))
            .setReadTimeout(Duration.ofSeconds(5))
            .build();
    }

위와 같이 RestTemplate 빈을 만드는데 RestTemplateBuilder를 이용해서 setConnectTimeoutsetReadTimeout 설정을 추가하여 타임아웃 설정을 추가할 수 있다.

setConnectTimeout은 서버에 연결하는 데 허용되는 최대 시간, 즉 네트워크 연결 시간 초과를, setReadTimeout은 클라이언트가 서버로부터 응답을 수신하는 데 허용되는 최대 시간, 즉 응답 시간 초과를 설정하는 데 사용되는 메소드이다.

RestTemplateBuilder는 Spring Framework에서 제공하는 클래스 중 하나로, RestTemplate 객체를 생성하고 구성하는 데 사용된다. 이러한 RestTemplateBuilder는 빌더 패턴을 사용하여 RestTemplate 객체를 생성할 수 있다.

이 클래스는 RestTemplate 객체를 구성하는 데 사용될 수 있는 여러 메서드를 제공하기 때문에 위 코드처럼 RestTemplate에 사용될 요청 팩토리, 인터셉터, 응답 핸들러 등을 구성할 수 있는 것이다.

이러한 메소드를 사용하여 타임아웃을 설정하면, 연결이나 응답이 지연되는 경우에도 클라이언트가 무한정 기다리지 않고 예외를 발생시켜 작업을 중단할 수 있기에 애플리케이션의 성능을 향상시키고, 사용자 경험을 개선할 수 있다.

RestTemplate Bean HTTP 요청 헤더 설정하기

RestTemplate은 HTTP 요청을 보낼 때 헤더를 설정할 수 있다. 헤더에는 요청을 보내는 클라이언트의 정보, 인증 토큰 등이 포함될 수 있다.

@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.set("Authorization", "Bearer " + accessToken);

    return restTemplateBuilder
            .setConnectTimeout(Duration.ofSeconds(5))
            .setReadTimeout(Duration.ofSeconds(5))
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
            .additionalInterceptors(new HttpHeaderInterceptor(headers))
            .build();
}

여기서는 defaultHeader() 메소드를 사용하여 기본 요청 헤더를 설정할 수 있다. 그리고 additionalInterceptors() 메서드를 사용하여 추가적인 인터셉터를 설정할 수 있다. 이를 통해 요청에 대한 헤더나 쿠키 등을 설정할 수 있게 된다.

위 코드의 HttpHeaderInterceptor는 직접 만든 인터셉터 클래스이다.

RestTemplate Bean 에러 핸들링 설정하기

RestTemplate를 통해 외부 API로 통신 요청을 할 때 중간에 에러가 발생하면 예외가 발생하게 되는데 이 때, 예외 처리를 해준다면 서비스의 안정성을 높일 수 있다.

예를 들어, 5xx 에러가 발생했을 경우, 서버에 문제가 있을 가능성이 높으므로 재시도를 하거나 다른 서버로 요청을 보내도록 처리할 수 있다.

@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
	return restTemplateBuilder
    	.setConnectTimeout(Duration.ofSeconds(5))
        .setReadTimeout(Duration.ofSeconds(5))
        .errorHandler(new RestTemplateResponseErrorHandler())
        .build();
    }}

위와 같이 RestTemplateConfig에 만들어진 RestTemplate 빈에 타임아웃, 요청 헤더, 에러 핸들링과 같은 설정을 적용해주면 안전하고 효율적인 API 통신을 할 수 있게 된다.

여기서 RestTemplateResponseErrorHandler이란 직접 만든 예외 처리 클래스이다.


RestTemplate 예외 처리하기

위에서 RestTemplateConfig에서의 RestTemplate Bean 설정을 살펴보았다. 이번에는 실제 서비스에서 RestTemplate을 통해 API 통신을 할 때 발생할 예외 처리에 대해서 이야기해보고자 한다.

보통 RestTemplate으로 API 통신하는 구문을 try/catch 블록으로 감싸서 예외를 처리하는 것이 일반적이다. 그러나 이 방법은 외부 API를 호출하는 메소드가 많아지면 많아질수록 중복 코드가 많이 발생하게 된다.

try/catch 블록을 통한 중복을 해결하기 위해 ResponseErrorHandler 인터페이스를 구현하는 방법이 있다. ResponseErrorHandler 인터페이스를 구현하는 방법은 예외 처리를 공통으로 관리하기 용이하다. 물론, RestTemplate을 구성하는데 그만큼의 추가적인 설정이 필요하다.

따라서, 복잡한 예외 처리를 해야하는 경우는 ResponseErrorHandler 인터페이스를 구현하는 것이 좋고, 단순한 예외 처리를 해야하는 경우는 try-catch 블록을 사용하는 것이 좋다.

이번에는 RestTemplate를 통해 외부 API에서 반환된 HTTP 오류를 정상적으로 처리하기 위해ResponseErrorHandler 인터페이스 를 구현하고 삽입하는 방법에 대해서 알아보려 한다.

기본 오류 처리

기본적으로 RestTemplate은 HTTP 오류가 발생한 경우 다음 예외 중 하나를 발생시키게 된다.

다음 예외는 RestClientResponseException의 확장 예외이다.

  • HttpClientErrorException: HTTP 상태 4xx의 경우
  • HttpServerErrorException: HTTP 상태 5xx의 경우
  • UnknownHttpStatusCodeException: 알 수없는 HTTP 상태의 경우

이제 ResponseErrorHandler 인터페이스의 구현체 클래스를 작성해보자.

exception.RestTemplateResponseErrorHandler.java

import org.springframework.data.crossstore.ChangeSetPersister;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResponseErrorHandler;

import java.io.IOException;

@Component
public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {

    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = (HttpStatus) response.getStatusCode();
        // HTTP 응답 코드가 4xx나 5xx인 경우 예외 상황으로 간주합니다.
        return statusCode.is4xxClientError() || statusCode.is5xxServerError();
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        HttpStatus statusCode = (HttpStatus) response.getStatusCode();
        // HTTP 응답 코드가 4xx나 5xx인 경우 처리할 수 있습니다.
        if(statusCode.series() == HttpStatus.Series.SERVER_ERROR) {
            // handle SERVER_ERROR
        } else if(statusCode.series() == HttpStatus.Series.CLIENT_ERROR) {
            // handle CLIENT_ERROR
        }
    }
}

코드를 살펴보면 응답에서 HTTP 상태를 읽어서 애플리케이션에 의미 있는 예외를 throw하여 던진다.

이제 RestTemplate Bean에 ResponseErrorHandler 구현체 클래스를 주입하여 예외 발생시 의도한 예외 처리를 할 수 있도록 해야한다.

config.RestTemplateConfig.java

import com.kakao.lango.resttemplatedemo.exception.RestTemplateResponseErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(5))
                .errorHandler(new RestTemplateResponseErrorHandler())
                .build();
    }
}

RestTemplateConfig 클래스에서 RestTemplateBuilder의 errorHandler() 메서드를 통해 직접 생성한 RestTemplateResponseErrorHandler를 추가하면 된다.

이렇게 되면 4xx, 5xx 결과 코드로 넘어오는 데이터도 HttpClientErrorException이 아닌 개발자가 원하는 방식으로 처리할 수 있게 된다.




Final..

프로젝트 설계를 어느정도 마무리하고 서버 애플리케이션 개발에 필요한 설정들을 알아보며, 구현 가능성 여부를 따지는 시간이 생각보다 오래 걸렸다.

4월 초까지는 Spring Boot 서버 애플리케이션 구축을 완료하고 React를 활용한 클라이언트 애플리케이션 개발을 시작해야 해서 점점 초조하고 조급해지고 있다.

또한, Velog에 기록으로 남기는 양이 점점 줄어들고 있음을 느끼고 있다. 물론 개발적인 요소보다 기획이나 설계적인 요소들을 많이 진행하고 있기 때문에 내용으로 남기기 애매한 부분이 더 크다.

그럼에도 불구하고 블로그 글의 양이 적다고 느낄 때면, 공부를 덜했구나 라는 생각이 드는건 어쩔 수 없는 것 같다. 21주 차에는 서버 애플리케이션을 개발하면서 본격적으로 부딪히는 고민들을 기록으로 잘 남겨두도록 해야겠다!



혹여 잘못된 내용이 있다면 지적해주시면 정정하도록 하겠습니다.

참고자료 출처

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글