반응형

스웨거란?

Swagger : Open Api Specification(OAS)를 위한 프레임 워크

FrontEnd와 BackEnd간 API 명세를 위해 사용한다고 생각하면 되며, 사용 시 페이지 하나에 기록되어있다.

하나의 협업 툴이라고 생각하면 되며, 기존 프로젝트는 Notion을 사용했었는데, 이번에는 Swagger를 채택하여 사용해보기로 했다.

의존성 추가

build.gradle 파일에 해당 dependency를 추가한다.

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

 

필자의 경우 Could not find org.springdoc:springdoc-openai-starter-webmvc-ui:2.2.0.Required by root project와 같은 에러메세지를 당도하여, 곤혹을 치뤘는데

 

build.gradle repositories에 해당 maven을 추가해준다.

maven { url 'https://repo.spring.io/snapshot' }

 

2018년도까지 업데이트 되었던 springfox를 사용하여 swagger를 작성했지만, 이제는 spring-openapi 라이브러리를 불러와 swagger를 작성한다. (2020년도에 springfox가 업데이트 되어 병행하는 경우도 있다.)

공식문서는 https://springdoc.org를 참조하자.

 

application.yml

springdoc:
  packages-to-scan: {스캔할 경로}
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    path: /
    disable-swagger-default-url: true
    display-request-duration: true
    operations-sorter: alpha

application.yml에 해당 코드를 추가한다.

pakages-to-scan에 해당하는 경로를 넣게되면 해당 경로를 상위폴더로 두고 하위폴더를 전부 탐색하니 생각하여 경로를 설정하도록 하자. 필자는 프로젝트 전체를 경로로 넣었다.

 

API가 RESTFUL하게 통신을 주고받기 때문에 json타입으로 기본 설정한다.

나머지는 기본 url을 사용할건지 등의 기본 설정이기에 해당 설정을 따른다.

 

Config 파일 추가

Bean에 등록하여 서버 실행 시 참조에 오류가 없도록한다.

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@OpenAPIDefinition(
        info = @Info(
                title = "LLM Service API",
                version = "1.0",
                description = "LLM Service API")
)
@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new io.swagger.v3.oas.models.info.Info()
                        .title("LLM Service API")
                        .version("1.0")
                        .description("LLM Service API"));
    }
}

@OpenAPIDefinition 어노테이션과 OpenAPI를 정의하여 Swagger의 제목과 버젼 설명을 명세한다.

 

이후 각 Controller, DTO, Entity, ResponseEntity(디폴트 응답 틀)에 설명을 추가한다.

 

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/llm")
@Tag(name = "Chat-GPT(LLM) 서비스", description = "LLM 관련 API")
public class LLMController {
    private final LLMService llmService;
    @Operation(summary = "코딩테스트 문제 생성 API", description = "난이도(1~5), 알고리즘, 추가 사항을 기반으로 코딩 테스트를 생성하는 API")
    @PostMapping("/code-generating")
    public ResponseEntity<LLMResponseDTO.CodeGenerateResponse> codeGenerator(@RequestBody LLMRequestDTO.codeGeneratingInfo request) {
        return llmService.codeGenerator(request);
    }

    @PostMapping("/chat")
    public ResponseEntity<LLMResponseDTO> chat(@RequestBody LLMRequestDTO.chatMessage request) {
        return llmService.chat(request);
    }
}

 

DTO

@Builder
@Getter
@AllArgsConstructor
@Schema(name = "LLMResponseDTO", description = "LLM 응답 양식", title = "LLMResponseDTO [LLM 응답 양식]")
public class LLMResponseDTO {
    @Builder
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @Schema(name = "CodeGenerateResponse", description = "코딩테스트 생성 응답", title = "CodeGenerateResponse [코딩테스트 생성 응답]")
    public static class CodeGenerateResponse{
        @Schema(name = "title", description = "문제 제목", example = "혼자 남지 않기 위한 최적의 경로 찾기")
        private String title;
        @Schema(name = "algorithm", description = "알고리즘", example = "DP")
        private String algorithm;
        @Schema(name = "content", description = "문제 내용", example = "<h3>혼자 남지 않기 위한 최적의 경로 찾기</h3>\n\n<p>이 문제는 '혼자 남지 않기 위한 최적의 경로 찾기'를 목표로 합니다. 주어진 2차원 격자에서 특정 조건을 만족하며 목적지에 도달하기 위한 행동을 결정해야 합니다. 주어진 조건을 만족하는 함수를 작성하고, 격자를 탐색하는 중 최대한의 안전성을 확보해야 합니다.</p>\n\n<h4>문제 설명:</h4>\n<p>이 문제는 특정 시작점에서 목적지까지 '혼자 남지 않기' 위한 최적의 경로를 찾아야 합니다. 2차원 격자에서는 특정 위치로 이동할 때 1의 스탭이 소요되며, 각 위치에서 움직일 수 있는 최대 경로 수를 제한하여 주어질 것입니다. 주어진 조건하에서 최단 경로를 결정해야 하며, 경로에 존재하는 위험 요소를 피하면서 격자를 탐색해야 합니다.</p>\n\n<h4>문제 제한 사항:</h4>\n<ul>\n<li>격자 크기: M x N (1 ≤ M, N ≤ 100)</li>\n<li>시작점: 출발 위치는 (0,0)으로 고정되어 있습니다.</li>\n<li>목적지: 목적지는 항상 오른쪽 아래 모서리 (M-1, N-1)입니다.</li>\n<li>위험 요소는 격자 내 임의의 위치에 존재할 수 있으며, 이를 피하는 것이 중요합니다.</li>\n</ul>\n\n<h4>입력 예시:</h4>\n<pre><code>\n5 5\n0 0 0 0 0\n0 -1 0 -1 0\n0 0 0 -1 0\n0 -1 -1 0 -1\n0 0 0 0 0\n</code></pre>\n\n<h4>출력 예시:</h4>\n<pre><code>\n7\n</code></pre>\n\n<h4>입출력 예 설명:</h4>\n<p>주어진 격자에서 (0,0)에서 (4,4)까지 가장 적은 스탭으로 이동하기 위한 경로는 7스탭 입니다. 중간에 -1은 이동할 수 없는 위치이며, 다른 안전한 경로를 찾아야 합니다.</p>")
        private String content;
        @Schema(name = "difficulty", description = "난이도", example = "3")
        private String difficulty;
        @Schema(name = "additionalNotes", description = "추가 사항", example = "문제의 조건을 만족하는 최소의 시간 복잡도를 갖는 알고리즘을 작성해줘.(can be null)")
        private String additionalNotes;
        @Schema(name = "testCases", description = "테스트 케이스 목록(front 필요 없을 확률이 높음.)")
        private List<TestCaseSpec> testCases;

        @Builder
        @Getter
        @AllArgsConstructor
        @NoArgsConstructor
        @Schema(name = "TestCaseSpec", description = "테스트 케이스 명세", title = "TestCaseSpec [테스트 케이스 명세]")
        public static class TestCaseSpec {
            @JsonProperty("input")
            @Schema(name = "input", description = "입력값", example = "5 5\n0 0 0 0 0\n0 -1 0 -1 0\n0 0 0 -1 0\n0 -1 -1 0 -1\n0 0 0 0 0")
            private String input;
            @Schema(name = "expectedOutput", description = "예상 출력값", example = "7\n")
            @JsonProperty("expected_output")
            private String expectedOutput;
        }


        public static LLMResponseDTO.CodeGenerateResponse of(String jsonResponse) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                JsonNode rootNode = objectMapper.readTree(jsonResponse);

                JsonNode problemInfoNode = rootNode.path("problem_info").get(0);

                List<TestCaseSpec> testCases = objectMapper.readValue(
                        rootNode.path("test_cases").toString(),
                        objectMapper.getTypeFactory().constructCollectionType(List.class, TestCaseSpec.class)
                );
                return LLMResponseDTO.CodeGenerateResponse.builder()
                        .title(rootNode.path("title").asText())
                        .content(rootNode.path("content").asText())
                        .algorithm(problemInfoNode.path("algorithm").asText())
                        .difficulty(problemInfoNode.path("difficulty").asText())
                        .additionalNotes(problemInfoNode.path("additional_notes").asText())
                        .testCases(testCases)
                        .build();
            } catch (Exception e) {
                throw new RuntimeException("Failed to parse JSON response"+ e.getMessage(), e);
            }
    }

 

ResponseEntity

@Data
@AllArgsConstructor
@Schema(name = "ResponseEntity", description = "전체 API 응답", title = "ResponseEntity [전체 API 응답]")
public class ResponseEntity<T> {
    @Schema(name = "statusCode", description = "응답 코드", example = "200")
    private Integer statusCode;
    @Schema(name = "message", description = "응답 메시지", example = "OK")
    private String message;
    @Schema(name = "data", description = "응답 데이터")
    private T data;

    public static <T> ResponseEntity<T> success(T data) {
        return new ResponseEntity<>(HttpStatus.OK.value(), "OK", data);
    }

    public static <T> ResponseEntity<T> error(Integer statusCode, String message, T data) {
        return new ResponseEntity<>(statusCode, message, data);
    }
}

 

이후 서버를 실행시킨 뒤, localhost:8080/swagger-ui/index.html# 로 접속한다.

 

메인 페이지

 

상세 페이지 Request
Response

 

이렇게 정의하여, 프론트에게 API 명세를 껴안아 주면 그때부터 완전한 프로젝트 협업의 시작이 되겠다.

 

반응형
반응형

먼저 필자는 코딩테스트 블로그를 만들고, 코딩테스트와 관련된 내용을 chatGPT에 질문하고, 코딩테스트 문제를 생성하는 서비스를 만드려고 한다.

사용하는 유저 혹은 문제 마다 다 다른 대화내용일텐데,
chatGPT를 사용하는 것처럼 질의응답 내용을 다 파악하여 답할 수 있을까?

 

결론은 그렇지 않다.

 

해당 내용을 기억하지 못하기 떄문에, 기존 대화 내용을 프롬프트에 담아 제공을 해준다.

이를 통해 chatGPT로 하여금 이전 내용을 숙지하여 의도에 맞는 답을 전달해줄 수 있도록 할 수 있는것이다. (물론 토큰 비용은 더 나가겠지만.. 좋은 서비스를 제공하기 위해서는 감안해야 할 듯 하다.)


따라서 나는 경우를 세가지로 나누게 되었다.

  1. 신규 사용자의 첫 질문
  2. 기존 사용자의 첫 코딩테스트 질문
  3. 기존 사용자의 연속된 코딩테스트 질문

경우마다 다 다르게 나누어 프롬프트를 설정한다.

프롬프트는 다음과 같다.

  1. 해당 언어를 주력 기술로 가지고 있는 사용자
  2. 기존 대화에서 현재 질문한 것과 가장 유사한 질문 및 가장 유사하지 않은 질문을 제시한다.
  3. 캐시 메모리를 이용하여, 가장 최근 두 질의응답을 저장해 이를 반환한다.

그렇다면 해당 행동은 RAG라고 볼 수 있을까?

RAG(Retrieval-Augmented Generation)는 외부의 정보를 검색하거나 불러와서 생성 모델의 입력으로 제공하는 방식으로, 본질적으로 검색(검색 및 불러오기) + 생성 접근 방식을 의미한다.

 

유의미한 결과를 도출하기 위해 정보를 다듬어 다시 제공한다는 점에서 RAG라고 볼 수 있다. 물론 주로 사용되고 있는 기술이 외부의 정보를 한데 모아 정보를 통합하여 질문하는 방식이다. 허나 유창한 기술을 사용하지 않더라도 (원칙에 부합하기도 하고) 사용자 입장에서 더 좋은 결과를 획득할 수 있다면 그것도 올바른 방법이지 않을까 필자는 생각한다. 

 

물론 2번 프롬프트에서 BERT모델을 사용하여 벡터화 한 값을 통해 유사한 질문과 가장 유사하지 않은 질문을 제시하기는 한다.

그렇다면 RAG를 어떻게 사용하게 되는지 확인해 보자.

 

  1. Retrieval (검색 및 불러오기)
    • 사용자가 이전에 했던 질문과 답변, 문제 정보 등을 DB(2번 프롬프트)나 캐시(3번 프롬프트)에서 불러온다.
    • 필요한 경우 중요한 부분만 요약하여 정보를 효율적으로 구성한다.(2번 프롬프트)
  2. Augmentation(정보 강화)
    • 불러온 대화 이력이나 문제와 관련된 정보를 프롬프트에 추가하여, 모델이 이전 대화 맥락을 인식할 수 있게 한다.
    • 요약된 이력, 사용자 성향, 특정 질문 패턴 등이 프롬프트에 포함되므로 사용자 맞춤형 답변을 생성하도록 도와준다.
  3. Generation(응답 생성)
    • 최종적으로 강화된 프롬프트를 모델에 입력하여 답변을 생성한다.
    • 이때, 모델은 전달된 프롬프트에 포함된 정보를 참고하여 맥락에 맞는 답변을 하게된다.

이렇게 발전에 흐름에 전지전능하다고 생각되는 GPT도 다 해주는 건 아니기에, 해당내용을 참고하여 API를 잘 다뤄보기 바란다.

 

 

반응형
반응형

Azure OpenAI가 반응속도가 느리고, 잘 작동하지 않아 유료지만, 일반 OpenAI chatGPT를 사용할 계획이다.

chatGPT playground에서 간단하게 설정해보자.

함수 설정, generate(beta)로 설정하기

generate에 지시사항을 넣고 실행하면 함수에 대한 설정이 가능하다. json형태로 제시되니 읽어보고 수정하면 된다.

함수가 추가된 모습

 

예시 설정

Assistant는 User의 질문에 대한 답변을 제시한다. 이를 원하는 형식으로 알맞게 조정할 수 있다.

 

 

반응형
반응형

Redis란?

먼저 Redis를 사용하는 이유에 대해서 알아야한다.

 

Redis는 캐시메모리에 최적화 되어있고, 반응속도를 빠르게 하는데 일가견이 있다.

벡터화된 데이터를 제공하기도 하여, 유사도 검색기능까지 구현할 수 있다.

 

이 외에도 제공하는 기능이 많으므로 알아보고 사용하면 되겠다.

 

Redis 설치 작업

homebrew로 redis 다운로드

brew install redis

위 명령어를 terminal에 입력한다. homebrew가 다운로드 되어 있어야 한다.

 

레디스 실행

brew services start redis

설치한 레디스를 실행시킨다.

레디스 클라이언트 접속 후 ping 명령어 입력

redis-cli
ping

redis-cli 명령어를 통해 레디스에 접속한 뒤, 켜져있는지 확인하기 위해 ping 명령어를 입력한다.

PONG이라는 답이 오면 잘 켜져있음을 확인할 수 있다.

 

SpringBoot와 연동

이후 스프링부트와 연동하도록 한다.

gradle을 주로 사용하기에 해당 dependency를 추가한다.

build.gradle에 추가.

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

 

이후 RedisConfig파일을 작성한다.

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        return template;
    }
}

Spring Boot에서 Redis와 상호작용하기 위해 RedisTemplate 또는 비동기 처리를 위한 ReactiveRedisTemplate을 사용할 수 있게 해준다.

반응형
반응형

Azure에서는 학생 인증을 진행 할 경우 100달러를 무료 지급하는 서비스를 진행하고 있다.

OpenAI를 이용한 API를 만들어보고 싶거나, 서비스를 맛보고 싶은 경우 진행하도록 하자.

채팅 플레이 그라운드 - chatGPT에 대한 설정을 진행할 수 있다.

MarketPlace에서 OpenAI를 검색하여 리소스를 생성하고 생성된 리소스에 들어온다.
이후 개요 탭에서 Azure OpenAI studio 이동을 누르면 해당 사진인 채팅 플레이 그라운드에 접속할 수 있게 된다.

해당 채팅 플레이 그라운드에서 지침 및 컨텍스트 제공을 통해 클라이언트에 답할 양식등을 지정할 수 있다.

 

배포를 해보도록 하자.

기본 모델과 미세 조정된 모델을 선택

배포 시, 기본 모델과 미세 조정된 모델을 선택할 수 있는데 나는 chatGPT의 기본모델을 선택하기로 하겠다.

제공하는 chatGPT 모델 종류

여기서 모델을 설정할 수 있다. 나는 가장 무난한 gpt-4를 사용하기로 했다.

모델 배포 상세 페이지

gpt-4 모델을 선택해서 배포유형에 따라 선택할 수 있으며, 리소스 위치 및 모델 버전 또한 사용자 지정으로 선택할 수 있다.

 

배포 유형은 총 네가지이다.

 

1. 글로벌 표준(Global Standard):

 전 세계적으로 서비스가 필요한 경우 선택하는 배포 유형입니다.

 다양한 지역에 걸쳐 안정적인 성능을 제공해야 할 때 유용합니다.

 글로벌하게 빠르고 일관된 응답 시간을 보장해야 하는 서비스, 예를 들어 전 세계적인 사용자 기반을 가진 서비스에 적합합니다.

2. 표준(Standard):

 특정 지역 또는 로컬 서비스에 적합한 배포 유형입니다.

 주로 지역적인 사용자 대상이거나 글로벌 성능이 필요 없는 경우 선택합니다.

 적은 비용으로 적절한 성능을 제공할 수 있습니다.

3. 전체 일괄 처리(Batch Processing):

 데이터를 한꺼번에 처리하는 작업에 적합한 배포 유형입니다.

 실시간 처리보다는 대규모 데이터를 일괄적으로 처리하는 것이 필요한 경우에 유용합니다. 예를 들어, 매일 밤 대량의 데이터를 처리하거나 로그 분석에 사용됩니다.

 실시간 성능보다는 작업 완료 시간이 중요한 경우에 선택합니다.

4. 프로비저닝 된 관리(Provisioned Management):

 사전에 리소스를 할당하고 관리하는 배포 유형입니다.

 특정 트래픽 패턴이 예측 가능하거나 고정된 리소스 할당이 필요한 경우 적합합니다.

 리소스 사용량을 미리 예상하고 설정할 수 있어, 필요에 따라 성능을 관리할 수 있습니다.

모델 배포 사용자 지정

 

이 중 한국에서 서비스할 예정이므로 표준을 선택하는 것이 일반적이다. 그래서 표준을 선택했으나, 남은 리소스가 오스트레일리아 혹은 캐나다 등과 같이 한국과 먼쪽에 위치하고 있으므로, 글로벌 표준으로 사용 하기로 한다.

 

글로벌 표준을 통한 chatGPT 모델 생성은 다음 포스팅에 기록하겠다.

반응형

'BackEnd > OpenAI chatGPT' 카테고리의 다른 글

chatGPT API를 사용할 때 유의할 점 (RAG)  (12) 2024.11.03
OpenAI chatGPT playground  (0) 2024.11.03
반응형

이전 로직에서는 api가 결국은 signin 페이지에 엔티티의 userid, userpw 그리고 상황 별 페이지 반환 값을 제공했다.

그러나 프론트 놈들이 message, success, property를 parameter로 받길 원해서 난관에 부딪혔다.

 

먼저 코드를 짤 때, 서로서로 원하고 주고받는 것이 무엇인지 생각하고 짜는 것이 중요해 보인다.


프론트에서 요청한 값은 다음과 같다.

userId와 userPw를 값으로 줬을 때, 해당하는 아이디와 비밀번호를 DB의 값과 비교하고,

여러 상황에 맞추어 "message" : "상황 별 설명", "success" : "성공 여부", "property" : "페이지 반환 값"

 

그렇다면, 상황은 4가지이다.

1) 아이디 / 비밀번호 일치

2) 아이디는 일치하나, 비밀번호가 불일치

3) 아이디 불일치

4) 자체적인 오류 (비교 불가)

 

각 상황마다 반환할 값을 부여해 보자.

 

1) 아이디 / 비밀번호 일치 - "message" : "로그인 성공", "success" : true, "property" : 200

2) 아이디는 일치하나, 비밀번호가 불일치 - "message" : "비밀번호 불일치", "success" : false, "property" : 301

3) 아이디 불일치 - "message" : "존재하지 않는 아이디", "success" : false, "property" : 302

4) 자체적인 오류 (비교 불가) - "message" : "로그인 중 에러 발생", "success" : false, "property" : 303


엔티티부터 수정해 보자.

@AllArgsConstructor
@ToString
@NoArgsConstructor
@Entity
@Getter
public class Signin {
    @Transient
    Integer property;
    @Transient
    Boolean success;
    @Transient
    String message;

    @Id
    @JsonIgnore
    private String userId;
    @Column
    @JsonIgnore
    private String userPw;

    public Signin(String userId, String userPw) { //엔티티 객체가 더 생성됨에 따라 생성자 생성
        this.userId = userId;
        this.userPw = userPw;
    }

    public void resultC(Signin signin){
        this.property = 200;
            this.success = true;
            this.message = "로그인 성공";
        }
    public void resultF1(Signin signin){
        this.property = 301;
        this.success = false;
        this.message = "비밀번호 불일치";
    }
    public void resultF2(Signin signin){
        this.property = 302;
        this.success = false;
        this.message = "존재하지 않는 아이디";
    }

    public void resultF3(Signin signin){
        this.property = 303;
        this.success = false;
        this.message = "로그인 중 에러 발생";
    }
}

- 어노테이션을 추가했다.

@Transient - 엔티티에 추가하나, DB에 추가하지 않을 값

@JsonIgnore - return값으로 반환하고 싶지 않을 때, 본 어노테이션을 추가하여 반환 값에 제외

 

- Signin이라는 퍼블릭 메소드를 만들었다. DTO파일에서 toEntity()라는 메소드를사용하는데 오류가 발생하였다. 엔티티 객체가 더 생성됨에 따라서 생성자가 따로 생성되는것이 아니라고 판단했고, toEntity()라는 메소드를 위한 생성자를 따로 만들어준 셈이다.

 

- 보내야 하는 결괏값에 따라 다른 값이 출력되도록 메소드를 만들어 주었다. 앞선 조건에 의한 값이 반환되도록 파라미터들을 최신화해주는 메소드가 되겠다.


다음은 컨트롤러파일을 건드려보자.

@RestController
@Slf4j
public class SigninApiController {
    @Autowired
    private SigninService signinservice;

    @PostMapping("/signin")
    public HttpEntity<Signin> signin(@RequestBody SigninForm dto){
        Signin signed = signinservice.login(dto);
        if(signed.getProperty() == 200) {
            return ResponseEntity.status(HttpStatus.OK).body(signed);}
        if(signed.getProperty() == 301)
            return ResponseEntity.status(301).body(signed);
        if(signed.getProperty() == 302)
            return ResponseEntity.status(302).body(signed);
        else return ResponseEntity.status(303).body(signed);

    }


}

간단하다. 이전 코드와 다른 점이라면 null값 유무에 따라 로그인의 여부만 반환했다면, 이번에는 단계적으로 구분 지어 값을 반환하게 된다.

사실상 컨트롤러에서는 주고받는 느낌이 강하여 로직은 service에서 확인하겠다.


마지막, 서비스다.

@Slf4j
@Service
public class SigninService {
    @Autowired
    private SigninRepository signinRepository;

    public Signin login(SigninForm dto) {

        Signin user = dto.toEntity();

        if(user.getUserId() == null || user.getUserPw() == null){
            // 잘못된 입력 303
            user.resultF3(user);
            return user;
        }

        log.info("id = {}, pw = {}", user.getUserId(),user.getUserPw());

        Optional<Signin> byUserId = signinRepository.findByUserId(user.getUserId());

        if (byUserId.isPresent()){
            Signin register = byUserId.get();
            if (register.getUserPw().equals(user.getUserPw())){ //등록되어 있는 비밀번호 .equals 유저가 입력한 비밀번호 비교
                // 비밀번호 일치 200
                user.resultC(user);
                return user;
            }
            else {
                // 비밀번호 불일치 301
                user.resultF1(user);
                return user;
            }
        }
        else {
            //조회 결과 없음 302
            user.resultF2(user);
            return user;
        }
    }
}

로직을 설명하겠다.

1) 잘못된 입력 - 303 입력받은 json데이터의 이름이 잘못되어 userId와 userPw가 반환되지 않는 경우

2) 200 전부 일치!

3) 301 비밀번호 불일치!

4) 302 아이디가 틀렸다!

 

엔티티에 등록한 메서드를 통해 반환해야 하는 값을 반환할 수 있도록 해준 것이 다다.

비교하는 로직 자체는 변화한 것이 없으며, 반환 값을 추가하는 메소드를 작성하여 덧씌운 것이 다다.

어렵게 생각하지 말자.

 


끝으로 내가 했던 실수들을 적어보자.

 

- return 하는 값에 여러 변수를 넣으려고 한 것 -> 리턴되어야 하는 변수 자체가 옵셔널로 바인딩되어있는 것도 확인 안 한 채로!

- signin 메소드에 변수를 넣으려고 한 것

- 잘 모르겠으면 내가 하고자 하는 것을 구교수님께 여쭤보도록 하자.

반응형

'BackEnd > Spring boot' 카테고리의 다른 글

스프링부트 - 간단한 로그인 #1  (9) 2021.07.11
반응형

이전에는 강의를 들으며 진행했지만 순전히 나의 힘으로 진행해 보기 위해 애를 썼다. 물론 강의를 조금 훑긴 했지만.

 

먼저 나는 웹이나 앱이나 여러 방면에서 내 서버가 쓰이기 위해 REST API를 바로 만들어 준다고 생각했다.

 

폴더를 api, dto, entity, repository, service 다섯 항목으로 구성했으며 내용은 다음과 같다.


먼저 Entity를 구현하여 로그인 기능에 사용될 변수들을 생성한다.

@AllArgsConstructor
@ToString
@NoArgsConstructor
@Entity
@Getter
public class Signin {
//    @Id
//    private String uuid; //uuid를 사용하려 했으나, ID값이 pk가 되어 사용 안함.

    @Id
    private String userid;
    @Column
    private String userpw;
//
//    public Signin(String userid, String userpw){ //AllArgsConstructor이 자동 생성
//   }

}

엔티티에 사용된 어노테이션들을 설명하겠다.

1) ArgsConstructor -

  • @NoArgsConstructor : 디폴트 생성자만 만들어준다.
  • @AllArgsConstructor : 모든 필드의 생성자를 만들어준다.
  • @RequiredArgsConstructor : 필수 생성자만 만들어준다.

이에 따라서 맨 아랫줄의 Signin은 @AllArgsConstructor로 인해 만들 이유가 없어진 것이다. 마찬가지로 오류 또한 발생했었다.

 

2)@ToString - 클래스 출력시 가지고 있는 엔티티 반환.

3)@Getter - 알아서 접근자 메소드를 생성한다. 후술 하겠으나, 덕을 좀 봤다.

4)@Entity - JPA가 관리할 엔티티임을 선언.

 

5) DB관련 어노테이션

@Id - pk임을 선언 [주의]❗️어노테이션 불러올 때, javax.persistence를 불러올 것. 오류가 발생했었다.

@Column 컬럼 매핑!

 

로그인 기능을 구현하기 위해서, 필요한 것이 아이디와 비밀번호 이외에 필요한 부분은 크게 없다. 물론 더 성장했을 경우 jwt토큰이나, 스프링부트 시큐리티를 활용하여 보안기능이 강력한 로그인을 제작할 수 있겠지만, 아직 내 수준은 초보에 머물러있기에 무작정 달려가보자.

 

각설하고 아이디와 비밀번호만 필요할 것 같아 관련 엔티티를 생성해 준 코드이다.


다음은 Repository이다. 엔티티가 DB를 생성했다면, 리파지토리는 DB에 접근하기 위해 메소드를 활용하게 하는 인터페이스이다!

public interface SigninRepository extends CrudRepository<Signin, Long> {
    Optional<Signin> findByuserid(String userid);

}

먼저 생각해낸 로직은 그렇다. 유저가 아이디와 비밀번호를 입력하면, 나는 입력한 유저아이디를 먼저 DB와 비교한다. 비교한 아이디가 DB에 존재하면 그제서야 비밀번호를 서로 확인하여 유저가 정상적으로 로그인 할 수 있음을 확인해준다.

 

따라서, findByuserid라는 메소드를 구현했다. userid를 받아서 Signin 엔티티에 먼저 담는것이다.


엔티티와 리파지토리를 구현했다면 서로를 연결하기 위한 dto가 필요하다.

@AllArgsConstructor
@ToString
public class SigninForm {
    private String userid;
    private String userpw;
    public Signin toEntity(){return new Signin(userid, userpw);}
}

dto를 엔티티로 바꿔주는 메소드 toEntity를 구현했다. 본 메소드를 사용하면 userid와 userpw를 받게 될것이다.


api 폴더 내부의 SigninApiController이다.

@RestController //REST API 컨트롤러임을 확인
@Slf4j // 로그 사용
public class SigninApiController {
    @Autowired // 자동 적용.
    private SigninService signinservice;

    @PostMapping("/signin") //signin이라는 주소 사용, post 사용.
    public ResponseEntity<Signin> signin(@RequestBody SigninForm dto){
        Signin signed = signinservice.login(dto); //signed는 로그인이라는 서비스 메소드로 확인
        return (signed != null) ? 
                ResponseEntity.status(HttpStatus.OK).body(signed) :
                ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }


}

@RestController로 RESTAPI임을 선언한다. 왜 RESTAPI를 사용하는가? 라고 묻는다면 대답해드리는게 인지상정!

간단히 말하자면, 세상에는 다양한 종류의 디바이스들이 있다. OS, IOS, WEB 기타등등 여러 디바이스에서 내가 만든 서버를 활용하기 위해서 활용되는 방식이다. "어떤 디바이스든 정해진 규격을 만들어 그 규격의 값을 제공받는다"가 주된 목표 되겠다. 디바이스가 다르다고 하더라도 값만 올바른 형식으로 받으면 되는 것 아닌가? 그래서 사람들은 RESTAPI라는 개념을 만들었고, 우리는 JSON이라는 형식을 주고받게 되었다.

 

말이 길어졌는데, 아무튼 나는 웹이나 앱 어디서든 쓰이기 위해 RESTAPI로 선언했다.

 

api로직은 그렇다. dto로 사용자 입력을 받고 서비스에서 엔티티로 변환한뒤 아이디 비교 비밀번호 비교를 실시하고, 다시 api로 돌아와 회원이면 HttpStatus.OK(200)과 사용자의 아이디 비밀번호를 그대로 제공해주고, 없을 경우 HttpStatus.BAD_REQUEST(400)을 제공하며 null값을 보낸다.


마지막 서비스이다.

@Slf4j
@Service
public class SigninService {

    @Autowired
    private SigninRepository signinRepository;

    public Signin login(SigninForm dto) {
        Signin user = dto.toEntity();
        log.info("id = {}, pw = {}", user.getUserid(),user.getUserpw());
		//입력된 아이디와 비밀번호 무엇인지!
        Optional<Signin> byuserid = signinRepository.findByuserid(user.getUserid());

        if (byuserid.isPresent()){
            Signin register = byuserid.get();
            if (register.getUserpw().equals(user.getUserpw())){ //등록되어 있는 비밀번호 .equals 유저가 입력한 비밀번호 비교
                // 비밀번호 일치
                return user;
            }
            else {
                // 비밀번호 불일치
                return null;
            }
        }
        else {
            //조회 결과 없음
            return null;
        }
    }
}

서비스에서는 주 기능을 구현해준다고 생각하면 된다.

로직 설명 들어가겠다. dto로 받은 유저의 정보를 엔티티화 하여 user로 받는다.

아까 엔티티에서 @getter 어노테이션을 선언하여 getUserid()라는 메소드가 자동생성되었다. 아주 꿀기능이라고 볼 수 있다.

그에 따라 user의 아이디와 비밀번호를 본 메소드로 찾아낼 수 있다.

 

byuserid라는 변수를 선언해준다. 리파지토리에 선언했었던 findByuserid 메소드를 활요해준다. dto로 받은 아이디를 DB에서 찾고 그것을 반환해주는 코드가 되겠다.

 

옵셔널로 바인딩 했기에 byuserid에 정보가 있을수도 없을 수도 있다. 왜 추상적인 값 옵셔널로 줬는가? byuserid에 정보가 있다면 DB에 ID가 있는것이고, 없으면 없는거지! 그러니 확인하기도 쉽다는 것이다. isPresent()를 사용하여 값이 있나 없나 확인한다.

 

register이라는 변수를 byuserid.get()으로 구현한다. register은 byuserid 엔티티의 모든 정보를 담는다.

 

.equals() 메소드를 활용한다! String값을 비교할때는 == 연산자가 아닌 equals메소드를 활용하자. 자 여기서 주의할점 register과 user의 차이점을 알겠는가? register은 DB에 등록되어있는 정보이고, user은 사용자로부터 입력받은 정보가 되겠다!! 따라서 getUserpw()메소드를 사용하여 비밀번호를 서로 비교해주자.


구현은 이로써 끝이 났다. DATA.sql을 resources 밑에 넣어 데이터를 여러가지 집어넣었고, Talend API Tester을 사용하여 정상작동하는지 확인했다.

Talend API에 해당값을 넣었더니 Response가 200으로 나왔다.

정상작동 하네용 ㅋㅋ 성공!!


로그인 기능 구현 중 다양한 오류 발생.

기깔나는 대화..

1) 카멜 케이스와 스네이크 케이스

UserID 는 오류가 난다. UserId로 설정해줘야한다. 왜? 카멜 케이스라서. 진짜 유의하기 바란다.

userId로 바꿨다. DB에서는 USER_ID로 설정되었다 왜? 자동적으로 대문자는 띄어쓰기로 생각하기 때문이라고 추측했다.

따라서 userid로 바꿔주었더니 그제서야 USERID로 들어갔다. 내가 만든 엔티티가 다 소문자인 이유이다.

끼.. 끼잉... 나는 이런 거 안배웠단 말야!!!

 

2) null값으로 데이터를 잡았었다.

이전코드는 진짜 난리가 났었다. null값으로 데이터를 잡아서 곤혹을 치뤘다..

 

아무튼 감만세 파이팅!

반응형

'BackEnd > Spring boot' 카테고리의 다른 글

스프링부트 - 간단한 로그인 수정 #1-1  (6) 2023.01.12

+ Recent posts