[인프런 워밍업 스터디 클럽] 0기 - 첫번째 발자국
첫번째 발자국
이 글은 자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]를 수강하고 인프런 워밍업 클럽에 참여하여 쓰는 첫번째 회고록입니다.
1주차 내용
1주차에서는 layered architecture로 구성한 api를 만드는 과정을 배웠다. 이 때, 그냥 완성된 코드를 떡하니 보여주고 알려주는 흔한 방식의 강의가 아니었다. 최태현 멘토님께서는 흔히 이야기하는 가장 간단한 "Hello World"수준의 코드에서 시작하여 문제
->해결
의 과정으로 api를 점진적으로 발전시켜가면서 설명해주셨다.
간단하게 요번주 수업 내용을 요약해보면 다음과 같다.
Day 1
OT를 하였다. 인프런 워밍업 클럽 진행 방식에 대해서 설명해주셨다.
Java의 역사도 핵심적인 부분을 설명해주시면서 모던 자바에 대한 중요성을 알려주셨다.
Day 2
서버, 네트워크와 HTTP 기초적인 부분을 "이세계" 비유법으로 설명해주셔서 추상적인 개념을 좀 더 와닿게 깨달을 수 있었다. 그리고 왜 HTTP를 사용하는지도 예시를 들어 설명하셔서 바로 수긍이 갈 수 있었다.
서버는 어떠한 기능
을 수행하는 것 -> 그 기능을 수행하길 원하면 요청
을 해야함. -> 요청은 인터넷, 네트워크
에서 이루어짐 -> 서로 다른 컴퓨터가 연결하려면 ip
, port
가 필요 -> 이 ip는 외우기 어려움 -> domain name(host) 로 해결, 이러한 시스템을 DNS
라 함.
(최태현님의 강의 방식은 이렇게 문제, 해결 순서로 기술을 설명하시고 + 비유로 추상적인 개념을 좀 더 빠르게 알 수 있게 해준다는 걸 알았고 어떻게 초점을 맞추어서 공부해야 할 지 알 수 있었다.)
데이터를 주고 받을 때 표준이 필요함 -> HTTP -> HTTP 역시 규칙이 있다. -> HTTP Method
(GET, POST, PUT, DELETE), Path
-> 요청을 보낼 때는 GET, DELETE는 쿼리, POST, PUT은 바디 / 응답은 상태코드
어떠한 요청을 보낼 때 이러한 부분들이 정해져 있어야 한다. -> API
-> 우리는 이 API개발을 하는 것
@RestController // 입구
public class CalculatorController {
@GetMapping("/minus") // HTTP method, path
public int addTwoNumber(@RequestParam int number1, @RequestParam int number2) { // 쿼리
return number1 - number2; // 반환값
}
}
위의 예제같이 api를 작성할 때, 두가지 규칙을 생각하며 작성하면 되었다. 1) api 설계 2) 확장성
api 설계
HTTP method
path
쿼리
반환값
확장성
위의 코드를 다음과 같이 변경이 가능하다.
@RestController // 입구
public class CalculatorController {
@GetMapping("/minus") // HTTP method, path
public int minusTwoNumber(CalculatorRequest request) { // 하나의 dto객체로
return request.getNumber1() - request.getNumber2(); // 반환값
}
}
public class CalculatorRequest {
private final int number1;
private final int number2;
public CalculatorRequest(int number1, int number2) {
this.number1 = number1;
this.number2 = number2;
}
public int getNumber1() {
return number1;
}
public int getNumber2() {
return number2;
}
}
파라미터가 많아지게 되면 신경써야 될게 많아지기 때문이다. 즉, 미래에 어떻게 변할지를 염두하며 코드를 짜야된다.
Day 3
Post API를 만들어 봤다. 다시 혼자 만들면서 에러를 만났다. 다음과 같았다.
@RestController // 입구
public class CalculatorController {
@PostMapping("/divide")
public int divideTwoNumber(CalculatorRequest request) {
return request.getNumber1() / request.getNumber2();
}
}
httpie를 이용하여 테스트를 하였더니 다음과 같은 응답메시지를 받았다.
$ http -v POST localhost:8080/divide number1=10 number2=3
"status"가 400으로 떴다. 뭐가 문제였을까? 400 status code는 클라이언트 에러를 나타낸다. RFC 9110참고.클라이언트 에러는 형식에 맞지 않는 요청 문법, 유효하지 않은 요청 메시지 형태이다. Post 요청은 GET과 다르게 body를 넘겨줘야 한다. 파라미터 부분을 @RequestBody
을 명시해주지 않아서 발생한 에러였다.
@RestController // 입구
public class CalculatorController {
@PostMapping("/divide")
public int divideTwoNumber(@RequestBody CalculatorRequest request) {
return request.getNumber1() / request.getNumber2();
}
}
그렇다면 0으로 나누는 에러를 일부러 발생시켜보니 다음과 같은 응답을 받을 수 있었다.
$ http -v POST localhost:8080/divide number1=10 number2=0
500 status code는 서버 에러를 나타낸다. RFC 9110참고. 서버 에러가 나면 다음과 같이 스택트레이스에 표시가 났다.
그리고 본격적인 어플리케이션 api를 개발하였다. 이 때, 어떤 데이터를 저장하는데 domain의 개념을 사용해서 저장할 객체를 새로 만들었다.
Day 4
기본적인 데이터베이스 사용법을 배웠다. 이 때, Day 3에서의 문제점(데이터 휘발성)을 알게 되었고 데이터베이스를 써야 하는 이유에 대해 제대로 알게 되었다.
Day 5
데이터베이스를 이용해 기존의 메모리에 저장하는 방식의 코드를 디스크에 저장하는 방식의 코드를 바꿔 api를 만들었다.
Day 6
클린 코드가 왜 필요한지에 대해 배웠다. 그냥 코드를 딱 보여주시는게 아니라 왜 클린코드를 지향해야 되고 리팩토링하는 과정을 세심하게 다뤄주셨다.
기존에 외우는 식으로 그냥 계층형 구조를 만들어서 코드를 짰다면, 이 강의를 지금까지 들으면서 왜 그렇게 해야하는지 와닿으며 코드를 짜니까 응용력이 확실히 전보다 생기는거 같다.
과제
백문이 불여일타란 말은 프로그래밍 분야에서의 유행어이다. 직접 쳐보는게 중요하다는 것이다. 하지만, 더 좋은 방법이 있다. 불여일시다. 그날 그날 바로 시험을 보는 것처럼 과제를 수행하니 코딩을 강제적으로라도 하게 되었다.
그리고 어떠한 개념에 대한 과제를 내주실 때도 최태현님께서 강조하시는 왜? 언제? 장단점을 고민해보게 했다.
과제 1
첫번째 과제: 어노테이션
어노테이션에 대한 궁금증이 사실 별로 있지 않았다. 궁금증이 없다는게 아니라 사실은 대충 넘어 갔었다는게 더 올바른 표현인거 같다.
하지만, 요번에 사용하는 이유를 조사해보고 나만의 어노테이션을 만들어 봄으로써 어노테이션을 구성하는 메타 어노테이션에 대해서 제대로 알게 되었다.
그 과정에서 spring의 대부분 어노테이션이 @Retention
이 RetentionPolicy.RUNTIME이란걸 알았다. spring은 Java의 Reflection api를 활용해 런타임에 동적으로 컴포넌트 스캔이 가능해야 되기 때문에 유지 정책을 런타임까지로 정한 것이다.
과제 2
두번째 과제: api 개발
여러 요구사항 제시해주셔서 응용력을 기를 수 있었다.
첫번째 문제를 발전시킨 과정은 다음과 같다.
@RestController
public class CalculatorController {
@GetMapping("/api/v1/calc")
public Calculator calculateTwoNumber(CalculatorResponse response) {
int add = response.getNum1() + response.getNum2();
int minus = response.getNum1() - response.getNum2();
int multiply = response.getNum1() * response.getNum2();
return new Calculator(add, minus, multiply);
}
}
컨트롤러에서 덧셈, 뺄셈, 곱셈의 로직을 처리하게 했다. 과제를 마감하고 최태현님께서 공통 피드백을 주셨다. 공통 피드백을 듣고 난 후 고쳐본 코드는 다음과 같다.
@RequestMapping("/api/v1")
@RestController
public class CalculatorController {
@GetMapping("/calc")
public CalculatorResponse calculateTwoNumber(CalculatorRequest request) {
return new CalculatorResponse(request);
}
}
멘토님의 피드백을 통해 Controller라는 클래스는 요청에 대한 응답을 처리해주는 역할만 하는 쪽으로 코드를 짜는게 좋을거 같다는 생각을 했다.
두번째 문제에서는 LocalDate타입의 객체를 @GetMapping을 달아준 메서드의 파라미터로 코드를 짰더니 바인딩이 안되는 문제가 있었다. 같이 활동을 하시는 분께서 버전문제일 거 같다고 알려주셨고 덕분에 해결할 수 있었다. 참고
과제 3
세번째 과제: 람다식에 대해 조사하기
람다식, 더 나아가 모던자바라고 불리우는 Java8에 관한 내용은 이제는 매우 필수적인 항목이다. 그러나, 계속 어려워 뒤로 미뤘었던 주제이다.
조사를 하고 예제를 매우 간단하게만 만들어봤다. 아직까지 익숙하게 체화가 되지 않은 상태인 거 같다. 코드를 직접 여러번 짜면서 모던자바 8에 대한 내용을 체화시키는 것을 요번 워밍업클럽 활동 중에 해야할 나만의 미션으로 정한 계기가 되었다.
과제 4
네번째 과제: api를 데이터베이스를 이용해서 구현하기
최종 코드는 다음과 같았다.
@GetMapping("/fruit/stat")
public FruitReadSalesAmountRespond readSalesFruitAmount(@RequestParam String name) {
String salesAmountSql = "SELECT SUM(price) FROM fruit WHERE name = ? GROUP BY is_sold";
List<Long> salesAmounts = jdbcTemplate.query(salesAmountSql, (rs, rowNum) -> rs.getLong(1), name);
Long salesAmount = salesAmounts.get(0);
Long notSalesAmount = salesAmounts.get(1);
return new FruitReadSalesAmountRespond(salesAmount, notSalesAmount);
}
혹시나 더 좋은 코드는 뭘까 생각 중이다.
과제 5
다섯번째 과제: 주어진 코드를 클린하게 리팩토링해보기
과제를 하면서 내가 짠 코드가 점점 산으로 간다는게 느껴졌다. 이유는 두가지였던 거 같다.
게임이란 것의 주체를 정확하게 파악을 안하고 무조건 class로 나눌려고만 함.
main 메서드에 대한 정확한 정의
그래서, 다시 처음 부터 다시 만들었고 코드는 다음과 같다.
public class DiceGame {
private static final int DICE_FACE = 6;
private final int[] resultCounts = new int[DICE_FACE];
public static void main(String[] args) {
System.out.print("숫자를 입력하세요 : ");
DiceGame game = new DiceGame();
game.startGame();
}
private void startGame() {
determineRollCounts(User.inputNumber());
printResult();
}
private void determineRollCounts(int a) {
for (int i = 0; i < a; i++) {
int randomDiceFace = (int) (Math.random() * DICE_FACE);
resultCounts[randomDiceFace]++;
}
}
private void printResult() {
for (int i = 0; i < resultCounts.length; i++) {
System.out.printf((i + 1) + "번 눈금이 %d번 나왔습니다.\n", resultCounts[i]);
}
}
}
public class User {
protected static int inputNumber() {
Scanner scanner = new Scanner(System.in);
return scanner.nextInt();
}
}
DiceGame
이란 class를 먼저 정의하고 기능, 역할을 나누니까 너무 간단하게 리팩토링이 되었다. 그리고 User
는 그냥 숫자만 넣어주는 행위밖에 안하기 때문에 다음과 같이 나눠봤다.
그리고 화룡점정으로 스트림을 이용하면,
public class DiceGame {
private static final int DICE_FACE = 6;
private final int[] resultCounts = new int[DICE_FACE];
public static void main(String[] args) {
System.out.print("숫자를 입력하세요 : ");
DiceGame game = new DiceGame();
game.startGame();
}
private void startGame() {
determineRollCounts(User.inputNumber());
printResult();
}
private void determineRollCounts(int a) {
IntStream.range(0, a)
.map(i -> (int) (Math.random() * DICE_FACE))
.forEach(randomDiceFace -> resultCounts[randomDiceFace]++);
}
private void printResult() {
IntStream.range(0, resultCounts.length).forEach(i ->
System.out.printf("%d번 눈금이 %d번 나왔습니다.\n", i + 1, resultCounts[i]));
}
}
다음과 같이 리팩토링 할 수 있었다.
댓글을 작성해보세요.