본문 바로가기
개발 공부

[Spring Boot] 스프링부트 실습 #1

by 주녕킴 2023. 9. 6.

과제 테스트를 위해 생전 해본 적도 없는 자바 API 개발을 일주일 안에 공부해서 시험을 봐야했다..

책을 사서 한 3일 정도 빠르게 훑으면서 스프링부트로 기본적인 CRUD를 구현해본 후, 드디어 시험을 하루 앞두고 있다.

공부한 내용을 블로그에 정리하고 싶었는데, 시간이 너무 촉박해서 이후로 미뤄뒀다.

 

사실 전에 한 번도 없던 전형이라 어떤 식으로 문제가 나올지 정말 막막했다..

프로그래머스의 2시간 짜리 API 개발 과제테스트로 환경 세팅을 연습해봤고,

https://school.programmers.co.kr/skill_check_assignments/430

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

유사한 형식으로 내 친구 Chat GPT에게 예상문제를 내봐달라구 했다.

예상 문제는 아래와 같다.

요청 사항: 간단한 계산기 API

API 서버를 구현하여 클라이언트가 두 숫자를 입력하고 원하는 연산을 수행할 수 있도록 하십시오.

✅ 구현 조건

  • API 서버로 보내지는 요청 형식은 다음과 같습니다.
    • URL: 0.0.0.0:5678/calculate
    • Method: POST
  • 요청 본문은 JSON 형식으로 다음과 같이 구성됩니다.
    jsonCo cod{]]{
    { "number1": 10,
    "number2": 5,
    "operation": "add"
    • "number1"과 "number2"는 두 개의 숫자입니다.
    • "operation"은 수행할 연산을 나타냅니다. 가능한 연산은 "add", "subtract", "multiply", "divide"입니다.
  • 정상 요청이 들어온 경우, 해당 연산을 수행하고 결과를 아래와 같이 응답합니다.
    jsonCopy code
    { "result": 15 }

✅ 요청 예시

bashCopy code
curl -X POST 0.0.0.0:5678/calculate \ -H
'Content-Type: application/json' \ -d
'{ "number1": 10, "number2": 5, "operation": "add" }'

✅ 응답 예시

jsonCopy code
{ "result": 15 }

이 문제를 해결하기 위해 Spring Boot 또는 Java의 다른 웹 프레임워크를 사용하여 API 서버를 구현하고, 클라이언트의 요청을 파싱하고 요청된 연산을 수행하는 로직을 작성해야 합니다. 또한, 예외 상황을 처리하고 적절한 응답을 반환하는 코드도 구현해야 합니다.

 

1. Entity 만들기

 

Entity는 세가지를 만들어줬다. 

  • Machine (프로젝트 이름을 calculator로 해버렸다..) : number1, number2, operation
package com.example.calculator;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString

public class Machine {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private Integer number1;
    @Column
    private Integer number2;
    @Column
    private String operation;
}
  • Response : message
package com.example.calculator;

import jakarta.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Response {
    private String message;
}

처음 접속했을 때 "message" : "server-check" 와 같이 응답을 보내기 위해 만들었다. 기존에 내장돼있는 이런 기능을 하는 메소드가 있는지 궁금..

  • SumResponse : result (계산의 결과값)
package com.example.calculator;

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

@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class SumResponse {
    private Integer result;
}

이것도 응답 형식을 맞춰주기 위해 만들었다.

 

2. DTO 만들기

POST로 입력을 받아서 DTO에 실어서 데이터베이스에 저장해야하기 때문에 만들었다. 

package com.example.calculator;

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

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class MachineDto {
    private Long id;
    private Integer number1;
    private Integer number2;
    private String operation;

    public Machine toEntity(){
        return new Machine(null,number1,number2,operation);
    }
}

Id의 경우 데이터베이스에서 자동으로 생성되게 해놨으므로 받아오지 않아도 된다고 배웠다. 

Entity로 변환하기 위한 toEntity 메소드도 만들어두었다.

 

3. Repository 만들기

사실 Repository를 만드는 과정은 아직 잘 숙지가 되지 않은 것 같다. 더 공부가 필요할 듯 하다.

일단은 그냥 제일 기본적인 것 같은(?) JPaRepository를 extends했다.

package com.example.calculator;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MachineRepository extends JpaRepository<Machine, Long> {
}

 

4. Service 만들기

초반에 배울 때는 Controller에서 모든 로직을 구현했었는데,

기능이 복잡해질 수록 이것을 Service에 위임하여 역할을 분담하는 것이 중요하다는 것을 배웠다.

그래서 이런 방식으로 나누는 습관을 들이려고 해봤다.

package com.example.calculator;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MachineService {

    @Autowired
    private MachineRepository machineRepository;

    public Machine create(MachineDto dto) {
        Machine machineEntity = dto.toEntity();
        return machineRepository.save(machineEntity);
    }

    public SumResponse calFunc(Machine machine) {
        Integer number1 = machine.getNumber1();
        Integer number2 = machine.getNumber2();
        String operation = machine.getOperation();

        if(number2==0 && operation.equals("divide") )
            throw new IllegalArgumentException("2로 나눌 수 없음!");

        if(operation.equals("add"))
            return new SumResponse(number1+number2);
        else if(operation.equals("subtract"))
            return new SumResponse(number1-number2);
        else if(operation.equals("multiply"))
            return new SumResponse(number1*number2);
        else if (operation.equals("divide") )
            return new SumResponse(number1/number2);
        else
            throw new IllegalArgumentException("연산자가 올바르지않음!");

    }
}
  • @Autowired : Repository나 Service를 사용하기 위해 객체를 주입!(생성 객체를 연결) 한다고 표현한다더라.. 
  • create 메서드 : 받아온 입력인 DTO를 받아서 repository를 통해 데이터베이스에 저장하는 역할을 한다.
  • calcFunc : 받아온 machine 객체의 매개변수들을 이용하여 실질적인 계산을 해서 반환하는 역할을 한다. SumResponse에 담아서 응답형식을 맞춰야 하므로 기본적으로 SumResponse 객체에 담아서 보냈다.
  • 오류 처리 : 나름대로 배웠던 예외처리를 적용해봤다. 나누기의 경우 number2(나누는 수)가 0이 될 때, 연산자가 올바르지 않은 경우 상황에 맞는 메시지와 함께 IllegalArgumentException을 던져주었다.

 

5. Controller 만들기

package com.example.calculator;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class MachineController {

    @Autowired
    private MachineService machineService;

    @GetMapping("/")
    public ResponseEntity<Response> index(){
        return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(new Response("server-check"));
    }

    @PostMapping("/calculate")
    public ResponseEntity<SumResponse> calculate(@RequestBody MachineDto dto){
        Machine machine = machineService.create(dto);
        SumResponse result = machineService.calFunc(machine);
        if(result == null){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
        log.info(result.toString());
        return ResponseEntity.status(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(result);
    }
}
  • 기본적으로 접속했을 때 응답을 주었다.
  • /calculate로 POST 요청을 보냈을 때, 우선 입력받은 값을 Service의 create 메서드로 보내서 machine 객체를 만들어주었다.
  • 만든 객체를 calcFunc 메서드의 매개변수로 보내서 계산 후 결과를 얻어올 수 있도록 했다. 
  • calcFunc 실행 결과 예외가 발생해서 null이 return 됐을 경우 BAD_REQUEST 응답을 보내주었고, result 값이 있을 경우 OK 응답과 함께 결과 값을 body로 보내주었다.

 

6. Test 해보기

사실 Test Code를 작성하는 것은 이전에 말로만 많이 들어봤지, 손을 대보진 못했던 영역이었다. 

그런데 과제 테스트에 있어 Test 과정이 굉장히 평가에 중요하다는 사실을 알고, 배워서 연습해보았다. 

크게 객체가 잘 만들어지는지, 계산 기능이 잘 작동하는지, 예외처리를 잘 하고 있는지 테스트해보았다. 

package com.example.calculator;

import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest

class MachineServiceTest {

    @Autowired
    private MachineService machineService;

    @Test
    void create_success() {
        Integer a=1;
        Integer b=2;
        String operation = "add";

        MachineDto dto = new MachineDto(null, a, b,operation);
        Machine expected = new Machine(1L, a,b,operation);

        Machine article  = machineService.create(dto);

        assertEquals(expected.toString(), article.toString());
    }

    @Test
    public void testAddition() {
        Machine machine = new Machine(null,5,3,"add");
        SumResponse result = machineService.calFunc(machine);
        SumResponse expected = new SumResponse(8);
        assertEquals(expected.toString(), result.toString());
    }

    @Test
    public void testSubtraction() {
        Machine machine = new Machine(null,5,3,"subtract");
        SumResponse result = machineService.calFunc(machine);
        SumResponse expected = new SumResponse(2);
        assertEquals(expected.toString(), result.toString());
    }

    @Test
    public void testMultiplication() {
        Machine machine = new Machine(null,5,3,"multiply");
        SumResponse result = machineService.calFunc(machine);
        SumResponse expected = new SumResponse(15);
        assertEquals(expected.toString(), result.toString());
    }

    @Test
    public void testDivision() {
        Machine machine = new Machine(null,5,3,"divide");
        SumResponse result = machineService.calFunc(machine);
        SumResponse expected = new SumResponse(1);
        assertEquals(expected.toString(), result.toString());
    }

    @Test
    public void testInvalidOperation() {
        Machine machine = new Machine(null,5, 0, "divide");

        // 예외가 발생하는 코드를 람다식으로 지정
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            machineService.calFunc(machine);
        });
    }
}
  • 처음에 테스트가 싹다 실패해서 몇번 좌절했었는데, 로그를 자세히 읽어보니 객체 그대로(SumResponse@2839819 이렇게,,) 반환되고 있는 것을 발견했다.
    • 알고보니 SumResponse에 @ToString을 해주지 않아서였다.. 해당 부분 수정해주니, 계산에 대한 테스트는 무사 통과!
  • 예외 처리를 올바르게 하고 있는지를 어떻게 테스트 해야할지 잘 몰랐었다. 
    • assertEquals말고도 다양한 기능이 있는 것을 알았고, 그 중에서도 Exception이 같은지 확인?? 하는 assertThrows를 사용할 수 있었다. 위 코드와 같이 람다식으로 사용할 수 있었고, 테스트 무사 통과!

이렇게 처음으로 혼자 아주아주~ 간단하지만 API 개발 문제를 풀어보았다. 

그래도 처음 접해본 것 치고는 3일만에 꽤나 성과 있?으심? ㅎㅎ..

 

사실 과제 테스트 뿐만 아니라 지금까지 프론트엔드 개발 공부만 하면서 백엔드 개발 공부는 한~참 뒤로 미뤄놓았는데,

맨날 불러다 쓰기만 하던 API를 이번 기회에 공부하고 만들어볼 수 있게 돼서 참 의미있었다. 나름 재미있는 것 같기도??

그리고 뭔가 자바는 진짜 찐 개발 천재들이 많이 써서 멋있어 보였는데 뿌듯하다,, 더 제대로 공부해보고 싶다. 

 

시험보면서 주의해야 할 것은!!

  1. 변수, 메서드 이름 직관적으로! 가독성 좋게 짓기
  2. 테스트 코드 꼭 작성해보기
  3. 예외 처리도 놓치지 말 것!
  4. 주석을 잘 달아보자!

현실적으로 스린이에게 지킬 수 있는 것은 이정도인 것 같다. 무엇보다 주어진 요구사항을 만족하는 게 우선이겠지만!

 

@Entity

@Getter 

@Id @GeneratedValue

PatchMapping에서 patch메소드가 잘 먹히지 않는 부분 -> getter, setter활용

@Transactional로 묶으면 하나의 트랜잭션으로 처리하여 이후 오류가 났을 때 롤벡

Repository, service 주입할 때 앞에 private 붙이기

@SpringBootTest

@SpringBootApplication

SpringApplication.run(App.class,args);

도 잊지말자~ 후후

 

아자아자 화이팅! 

 

'개발 공부' 카테고리의 다른 글

[React] Do it! 리액트 모던 웹개발(with 타입스크립트)  (1) 2023.01.25
웹 지식 학습 - part 3  (0) 2022.07.29
웹 지식 학습 - part 2  (0) 2022.07.29
웹 지식 학습 - part 1  (0) 2022.07.29
HTML & CSS 실습1  (0) 2022.07.10