청약챌린지 3주차이다. 3회차 미션을 진행하며 가장 크게 느낀 점은, 코딩테스트 공부에서 중요한 것은 '매일 엄청난 양을 푸는 것'이 아니라 '흐름이 끊기더라도 다시 공부방으로 돌아오는 루틴을 만드는 것'이다. 그리고 코드트리가 제공하는 여러 동기부여 장치들이 이 루틴을 유지하는 데 매우 현실적인 도움이 되었다.
1. 학습 의지를 깨우는 카톡 리마인더
일과가 바쁘다 보면 "오늘 알고리즘 문제 풀어야지" 하는 생각조차 잊어버릴 때가 많다. 그때 도착하는 코드트리의 알림톡 리마인더는 공부 흐름이 완전히 끊기지 않도록 붙잡아주는 역할을 해준다.
설령 시간이 부족해 문제를 직접 풀지 못하는 날이더라도, 알림을 보고 접속하여 개념 설명을 다시 읽거나 이전 오답 해설을 복기하는 식으로 학습 밀도를 유지할 수 있다.
이런식으로 알림이 온다.
2. 깃허브(GitHub) 잔디 심기 공식 연동
개발자로서 기술 역량을 기록하고 꾸준함을 증명하는 데 있어 GitHub의 초록색 잔디는 매우 직관적인 지표이다.
기존에는 외부 확장 프로그램을 사용하여 풀이를 연동하곤 했는데, 코드트리는 플랫폼 내부에서 공식적으로 Repository 연동 및 폴더명 커스텀 설정을 지원하여 굉장히 편리하다.
5월달은 코드트리 덕분이라 해도 과언이 아니다.
제출한 정답 코드가 자동으로 GitHub에 커밋되는 것을 보며, 내가 투자한 시간이 증발하지 않고 기록으로 남는다는 것을 확실히 느낄 수 있었다. 가장 좋은 점은 문제가 폴더별로 깔끔히 정리되어 내가 풀었던 문제들을 직관적으로 볼 수 있다는 점이다.
맨 왼쪽 사진처럼 문제가 폴더별로 정리되고, 해당 폴더에 들어가면 자동으로 README에 남겨진다.
내가 어떻게 풀었는지 자동으로 기록된다.
3. 고립감을 해소하는 라이브 챌린지 현황 및 단체 미션
혼자서 문제를 풀다 보면 쉽게 지치기 마련이다. 하지만 코드트리 대시보드에서는 다른 참여자들이 실시간으로 문제를 풀고 학습하는 현황이 라이브로 업데이트되어 자연스러운 자극을 받을 수 있다.
특히 이번 3회차에는 공동 경험치 10만을 모아 함께 보상을 받는 단체 미션이 존재하여, 개인 학습임에도 불구하고 '다 함께 참여하고 있다'는 유대감을 느낄 수 있었다.
이번 3회차를 하면서 느낀점은 코드트리의 시스템이 꾸준한 학습 습관을 유지하는데 분명히 도움되는 플랫폼이라는 점이다.
학업과 과제, 프로젝트를 병행하다 보면 현실적으로 변수가 많아서 쉽지 않겠지만, 코드트리의 알림톡과 동기부여 장치들을 활용해 매일 작은 문제라도 풀어나가며 문제 푸는 습관을 형성할 계획이다. 4주차까지 아자아자!
Functions부터 시작해서 Recursive Functions → Sorting → Simulation → Exhaustive Search → Case Work → Ad-Hoc 순서로 이어진다.
Trail은 선택할 수 있어서 자신의 수준이나 목표에 맞게 시작점을 고를 수 있다.
단순히 문제만 나열하는 방식이 아니라, 각 챕터 안에서 기본 → 연습 → 테스트 순서로 문제가 구성되어 있어서 개념을 익히고 바로 적용해보는 흐름이 자연스럽게 만들어진다.
처음에는 이 구조가 그냥 문제 묶음처럼 보였는데, 직접 따라가보니 기본 문제에서 개념을 잡고, 연습 문제에서 조건이 조금씩 붙고, 테스트 문제에서 내가 진짜 이해했는지 확인하는 흐름이 꽤 잘 설계되어 있다는 걸 느꼈다.
Sorting 학습에서 달라진 풀이 감각
실제 학습 화면
기본 문제에서 Arrays.sort() 감각을 익혔다.
예를 들어 기본 문제인 오름내림차순 정렬은 이렇게 풀었다.
Arrays.sort(arr);
for (int i = 0; i < n; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
for (int i = n - 1; i >= 0; i--) {
System.out.print(arr[i] + " ");
}
처음엔 내림차순을 따로 다시 정렬해야 하는 줄 알았는데,
오름차순 정렬 후 역순으로 출력하면 된다는 걸 이때 정리했다.
정렬은 쉬워 보이지만 막상 문제에 적용하려고 하면 헷갈리는 부분이 생겼다. 단순히 오름차순, 내림차순 정렬만 하면 되는 줄 알았는데, 기준이 두 개 이상이거나 특정 조건에 따라 정렬 방식이 바뀌는 경우에는 처음 접근 자체가 달라져야 했다.
기본 문제에서는 Arrays.sort() 같은 내장 함수를 활용하는 감각을 익혔고, 연습 문제로 넘어가면서 comparator를 직접 구현하거나 정렬 기준을 세분화하는 연습을 했다. 테스트 문제에서는 정렬만으로 해결이 되는 문제인지, 아니면 정렬 이후 추가 처리가 필요한지를 판단하는 게 중요했다.
이 과정에서 정렬 문제를 볼 때 "뭘 기준으로 정렬할 것인가"를 먼저 정리하고 코드를 작성하는 습관이 조금씩 생겼다.
기본 → 연습 → 테스트를 따라가며
코드트리에서 좋았던 점은 같은 개념을 반복하되, 매번 조금씩 다른 방식으로 적용해볼 수 있다는 점이었다. 기본 문제만 풀었을 때는 "이거 알겠는데?" 싶었는데, 테스트 문제에서 막히는 경우가 있었다. 그때 내가 개념을 몰라서 틀린 건지, 구현을 실수한 건지를 구분할 수 있어서 다음에 뭘 보완해야 할지가 명확해졌다.
해설과 모범코드를 내 코드랑 비교해보는 것도 도움이 됐다. 같은 결과를 내더라도 훨씬 간결하게 쓰는 방식을 보면서 내 풀이에서 불필요한 부분이 어디인지 확인할 수 있었다.
다른 서비스와 비교했을 때
백준이나 프로그래머스는 문제는 많지만 내가 뭘 먼저 풀어야 할지 기준이 없어서, 결국 아무 문제나 풀다가 방향을 잃는 경우가 많았다. 코드트리는 커리큘럼 순서가 잡혀 있어서 일단 따라가기만 하면 된다는 점이 달랐다. 특히 어떤 유형이 약한지 갭체크로 확인하고 거기서부터 시작할 수 있다는 게 실질적으로 도움이 됐다.
배포하면서 okta.oauth2.audience 를 배포 도메인으로 바꿨더니 401 에러가 발생했다. Auth0 API의 Identifier(http://localhost:8080)와 설정값이 달라서 토큰 검증이 실패한 것이었다. Identifier는 변경이 불가능하므로 Spring Boot와 React 설정을 다시 http://localhost:8080 으로 맞춰서 해결했다.
HTTP 요청코드 - 클라이언트가 보낸 요청이 성공했는지 실패했는지 알려주는 코드. - 응답은 100~500번대까지 5개의 그룹으로 나뉘어져 있다.
상태 코드
설명
1XX(정보)
요청이 수신돼 처리 중입니다.
2XX(성공)
요청이 정상적으로 처리됐습니다.
3XX(리다이렉션 메시지)
요청을 완료하려면 추가 행동이 필요합니다.
4XX(클라이언트 요청 오류)
클라이언트의 요청이 잘못돼 서버가 요청을 수행할 수 없습니다.
5XX(서버 응답 오류)
서버 내부에 에러가 발생해 클라이언트 요청에 대해 적절히 수행하지 못했습니다.
요청이 성공했을 때 200, 데이터 생성을 완료했을 때는 201 을 반환.
요청한 정보를 찾을 수 없을 때 404, 서버에 오류가 났을 때 500 을 반환.
서버는 클랄이언트의 요청에 대한 응답으로 화면(view)가 아닌 데이터(data)를 사용하는데, 이때 사용하는 응답 데이터가 JSON(JavaScript Object Notation) 이다.
[JSON 데이터 예시]
{
"id" : 1,
"name" : "Park",
// 키 값으로 또 다른 JSON 데이터와 배열 사용 가능
address : {
"Street": "Nambu Street 151",
"suite": "Central Villat 301",
...
},
"likes" : {"singing", "reading", "writing"}
}
REST API 의 응답 표준으로 사용하는 JSON 은 키와 값의 쌍으로 된 속성으로 데이터를 표현한다.
JSON 의 값으로 또 다른 JSON 데이터나 배열을 넣을 수 있다.
REST API 의 구현 과정
REST : HTTP URL로 서버의 자원(resource) 을 명시하고, HTTP 메서드(POST, GET, PATCH/PUT, DELETE) 로 해당 자원에 대해 CRUD(생성, 조회, 수정, 삭제) 하는 것을 의미한다.
API : 클라이언트가 서버의 자원을 요청할 수 있도록 서버에서 제공하는 인터페이스(interface).
REST API : REST 기반으로 API를 구현한 것. -> 클라이언트가 기기에 구애받지 않고 서버의 자원을 이용할 수 있다.
REST API 를 구현하려면 URL (REST API의 주소) 을 설계해야 한다.
앞서 만든 Article 데이터를 CRUD(생성, 조회, 수정, 삭제) 하기 위해 REST API 주소를 다음과 같이 설계한다.
[REST API 설계]
HTTP Method
REST API URL
GET (목록조회)
/api/articles
GET (단일조회)
/api/articles/{id}
POST (생성)
/api/articles
PATCH (수정)
/api/articles/{id}
DELETE (삭제)
/api/articles/{id}
설계를 끝냈다면, RestController를 생성한다.
[api/ArticleApiController]
@RestController
public class ArticleApiController {
@Autowired
private ArticleRepository articleRepository;
// GET
@GetMapping("/api/articles")
public List<Article> index() {
return articleRepository.findAll();
}
Article 묶음을 반환하므로 반환형이 List<Article>인 index() 메서드를 정의한다.
return 문에는 articleRepository 의 findAll() 메서드를 사용해 DB 에 저장된 모든 Article을 가져와 반환한다.
@Autowired : articleRepository 를 선언하기 위해 의존성을 주입한다.
[API tester]
GET 요청과 해당 URL 주소로 요청하면 JSON 형식으로 목록값이 잘 반환된다.
이제 단일값을 조회해보자.
[api/Article/ApiController]
// POST
@GetMapping("api/articles/{id}")
public Article show(@PathVariable long id) {
return articleRepostory.findById(id).orElse(null);
}
@RequestBody : 요청 시 본문(Body)에 실어 보내는 데이터를 create() 메서드의 매개변수로 받기위해 선언.
받은 dto를 DB에서 활용할 수 있도록 Entity로 변환해 article 에 넣고, articleRepository를 통해 DB에 저장.
[API tester]
POST 요청 성공.
PATCH, 수정 구현은 아래와 같다.
[api/ArticleApiController]
// PATCH
@PatchMapping("api/articles/{id}")
public ResponseEntity<Article> update (@PathVariable long id, @RequestBody ArticleForm dto) {
// 1. DTO -> Entity 변환하기
Article article = articleRepository.toEntity(); // 서비스 통해 게시글 수정
// 2. 타깃 조회하기
Article target = articleRepository.findById(id).orElse(null);
// 3. 잘못된 요청 처리하기
if (target == null || id !=article.getId()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(null);
// 4. 업데이트 및 정상 응답(200) 하기
target.patch(article)
Article updated = articleRepository.save(target);
return ResponseEntity.status(HttpStatus.OK).body(updated);
}
ReponseEntity : Rest 컨트롤러의 반환형, 즉 RESP API 의 응답을 위해 사용하는 클래스이다. - REST API 요청을 받아 응답할 때 이 클래스에 HTTP 상태코드, 헤더, 본문을 실어 보낼 수 있다.
HTTPStatus : HTTP 상태 코드를 관리하는 클래스로, 다양한 Enum 타입과 관련한 메서드를 가진다. - 열거형으로, 여러 상수로 이루어진 고정 집합을 가진다. - 200 : HTTPStatus.OK, - 201 : HTTPStatus.CREATED - 400 : HTTPStatus.BAD_REQUEST
수정할 내용이 있는 경우에만 수정 가능하도록 patch() 메서드를 만들어준다.
[entity/Article]
public void patch(Article article) {
if (article.getTitle() != null) {
this.title = article.title;
if (article.getContent() != null) {
this.content = article.content;
}
}
}
if 문으로 article(수정 Entity) 의 title 이 null 이 아닐 경우에만, this(target) 의 title을 갱신해 준다.
*content 또한 동일.
[API tester]
PATCH 요청 성공.
PATCH URL 의 id('1') 변수와 BODY 부분의 id ('1') 요청이 일치해야 한다.
마지막으로 DELETE 이다.
[api/ArticleController]
// DELETE
@DeleteMapping("/api/articles/{id}")
public ResponseEntity<Article> delete (@PathVariable long id) {
// 1. 대상 찾기
Article target = articleRepository.findById(id).orElse(null);
// 2. 잘못된 요청 처리하기
if (target == null) {
return ResponseEntity.status(HTTPStatus.BAD_REQUEST).body(null);
}
// 3. 대상 삭제하기
articleRepository.delete(target);
return ReponseEntity.status(HTTPStatus.OK).build();
}
잘못된 요청일 경우 ResponseEntity 에 BAD_REQUEST, 본문(body) 에는 null 을 실어보낸다.
잘못된 요청이 아닐 경우 대상 Entity 를 삭제한다.
그리고 HTTPStatus.OK, 본문(body) 에는 null 대신 body 응답이 없는 객체를 생성한다. (즉, null과 동일)
삭제 요청을 받은 컨트롤러는 repostiory를 통해 DB에 저장된 데이터를 찾아 삭제한다.(데이터가 있는 경우)
삭제가 완료됐다면 클라이언트를 결과 페이지로 리다이렉트 한다. - 클라이언트에 삭제 완료 메시지도 띄어주기 위해 RedirectAttributes 클래스를 사용한다. - 해당 클래스의 addFlashAttribute() 메서드는 리다이렉트 된 페이지에서 사용할 일회성 데이터를 등록할 수 있다.
2. 데이터를 수정해 DB 에 반영한 후 결과를 볼 수 있게 <상세 페이지>로 리다이렉트하기.
1딘계 동작원리2단계 동작원리
1단계부터 구현해보자.
상세페이지에 edit(수정) 으로 이동하는 링크를 연결한다.
[articles/show.mustahce]
<a href="/articles/{{article.id}}/edit" class="btn btn-primary">Edit</a>
<a href="/articles">Go to Article List</a>
href 속성 값의 URL을 보면 id가 article의 속성이므로 {{article.id}} 로 표시했다.
{{#article}, {{/article}} 형식으로 지정한 경우 -> {{id}} 만 써도 되지만 그게 아니므로 '.' (점)을 사용하여 표시한다.
이제 Edit 요청을 받을 컨트롤러를 연결한다.
[controller/ArticleController]
@GetMapping("/articles/{id}/edit")
public String edit(@PathVariable Long id, Model model) {
// 수정할 데이터 가져오기
Article articleEntity = articleRepository.findById(id).orElse(null);
// 모델에 데이터 등록하기
model.addAttribute("article", articleEntity);
// 뷰 페이지 생성하기
return "articles/edit";
}
@PathVariable : id 값을 변수로 사용하여 가져오기 위해 annotation 설정.
Model : 객체를 담을 모델을 사용하기 위해 메서드의 매개변수로 설정.
수정할 데이터 가져오기 : articleRepository의 findById(id) 메서드로 데이터를 찾아 가져온다. - 만약 데이터를 찾지 못하면 null을 반환하고, 데이터를 찾았다면 Aticle 타입의 articleEntity 로 저장한다.
모델에 데이터 등록하기 : articleEntity를 article로 등록.
controller 를 생성했으니 이제 DB 에 저장된 데이터를 수정페이지로 가져와 화면에 출력해보자.
※<input> 태그로 생성한 입력 요소의 초기값은 value 속성으로 정의하지만, <textarea> 태그로 생성한 여러줄의 입력 요소는 콘텐츠 영역에 초깃값을 정의한다,
화면 출력 모습 (title, content 가 정상 출력된다.)
클라이언트와 서버 간 처리 흐름을 4가지로 나누어 생각해보자.
MVC(Model-View-Controller) : 서버 역할을 분담해 처리하는 기법
JPA(java Persistence API) : 서버와 DB 간 소통에 관여하는 기술
SQL(Structured Query Language) : DB 데이터를 관리하는 언어
HTTP(HyperText Transfer Protocol) : 데이터를 주고받기 위한 통신 규약
HTTP 메서드- 클라이언트와 서버 간에 데이터를 전송할 때는 통신 규약 즉, 프로토콜을 따른다. - 프로토콜(protocol) 은 기기 간에 각종 신호 처리 방법, 오류 처리, 인증 방식 등을 규정하고 있다. - 그 중 HTTP는 웹 서비스에 사용하는 프로토콜로, 클라이언트의 다양한 요청을 메서드를 통해 서버로 보낸다. - 대표적인 메서드는 POST(데이터 생성 요청), GET(조회), PATCH/PUT(수정), DELETE(삭제) 이다.
데이터 관리
SQL
HTTP
데이터 생성(Create)
INSERT
POST
데이터 조회(Read)
SELECT
GET
데이터 수정(Update)
UPDATE
PATCH(PUT)
데이터 삭제(Delete)
DELETE
DELETE
이제 2단계이다.
2단계는데이터를 수정해 DB 에 반영한 후 결과를 볼 수 있게 <상세 페이지>로 리다이렉트 해야한다.
※ <form> 태그는 옛날에 만들어진 규격이라 PATCH 메서드를 지원하지 않기 때문에 POST로 요청한다.
수정 폼에서 서버로 보낼 때는 id 값을 보내야 하므로 type="hidden" 속성으로 숨겨서 전송한다.
[dto/ArticleForm]
@AllArgsConstructor
@ToString
public class ArticleForm {
private Long id;
private String title; // 제목을 받을 필드
private String content; // 내용을 받을 필드
public Article toEntity() {
return new Article(id, title, content);
}
}
수정 폼에서 id를 추가했으므로, DTO에도 id를 추가한다.
[controller/Articlecontroller]
@PostMapping("/articles/update")
public String updateArticle(ArticleForm form) {
log.info(form.toString());
// 1. DTO를 엔터티로 변환하기
Article articleEntity = form.toEntity();
log.info(articleEntity.toString());
// 2. 엔터티를 DB에 저장하기
// 2.1 DB에서 기존 데이터 가져오기
Article target = articleRepository.findById(articleEntity.getId()).orElse(null);
// 2.2 기존 데이터값 갱신하기
if (target != null) {
articleRepository.save(articleEntity); // 엔터티를 DB에 저장(갱신)
}
// 3. 수정 결과 페이지로 리다이렉트하기
return "redirect:/articles/" + articleEntity.getId();
}
수정 폼에서 전송한 데이터는 DTO로 받기 때문에, ArticleForm을 매개변수로 추가한다.
DTO-> Entity로 변환하는 form.toEntity() 메서드를 호출해 반환값을 articleEntity 이름으로 받는다.
데이터 생성이 아닌, 수정하기 위해 기존 데이터를 가져온다. - 이때는 리파지토리를 이용하여 articleRepository.findById() 메서드를 호출한다. - findById() 메서드는 repository에서 자동 제공하는 메서드로, 괄호 안에는 찾는 id값을 작성한다. - findById(articleEntity.getId()) 메서드를 호출해 반환받은 데이터를 Article 타입의 target 변수에 저장한다. - 데이터가 없다면 null 을 반환한다.
기존 데이터를 가져오고 나면 articleRepository.save() 메서드로 기존 데이터를 갱신한다.
데이터를 수정하면 수정된 내용이 반영된 상세 페이지로 이동하도록 redirect 시켜준다. -URL의 id 부분은 Entity에 따라 매번 바뀌어야 하므로, articleEntity의 getId() 메서드를 호출한다.
디버깅 로그'가가가가' -> '가나다라' 수정.
redirect 페이지인 /articles/1 로 이동하고, 제목과 내용도 수정한 대로 잘 바뀌었다. 성공!
@GetMapping("/articles/{id}")
public String show(@PathVariable Long id, Model model) { // 매개변수로 id 받아오기
log.info("id = " + id); // id 잘 받아오는지 확인
@PathVariable : URL 요청으로 들어온 전달값을 컨트롤러의 매개변수로 가져온다.
// 1. id를 조회해 데이터 가져오기
Article articleEntity = articleRepository.findById(id).orElse(null);
findById(id).orElse(null) : id 값으로 데이터를 찾을때 id값이 없으면 null을 반환.
@GetMapping("/articles/{id}")
public String show(@PathVariable Long id, Model model) { // 매개변수로 id 받아오기
log.info("id = " + id); // id 잘 받아오는지 확인
// 1. id를 조회해 데이터 가져오기
Article articleEntity = articleRepository.findById(id).orElse(null);
// 2. 모델에 데이터 등록하기
model.addAttribute("article", articleEntity);
Model : MVC 패턴에 따라 조회한 데이터를 뷰 페이지에서 사용하기 위해 객체로 받는다.
model.addAttribute : 모델에 데이터를 등록한다.
@GetMapping("/articles/{id}")
public String show(@PathVariable Long id, Model model) { // 매개변수로 id 받아오기
log.info("id = " + id); // id 잘 받아오는지 확인
// 1. id를 조회해 데이터 가져오기
Article articleEntity = articleRepository.findById(id).orElse(null);
// 2. 모델에 데이터 등록하기
model.addAttribute("article", articleEntity);
// 3. 뷰 페이지 반환하기
return "articles/show";
}
'#' 과 '/' 로 범위를 정하고, 모델 article에 담긴 객체를 클라이언트에서 출력하기 위해 '{{}}' 를 사용한다.
model 에 데이터가 담겨있다는 가정하에 해당 controller URL 로 조회하면
데이터가 성공적으로 조회된다.
이제 단일값이 아닌 list를 조회해보자.
위에서는 Entity를 반환했다면, 목록을 조회할 때는 엔티티의 묶음인 list를 반환한다.
[controller/ArticleController]
@GetMapping("/articles") // 다중데이터 목록 조회
public String index(Model model) {
// 1. 모든 데이터 가져오기
ArrayList<Article> articleEntityList = articleRepository.findAll();
// 2. 모델에 데이터 등록하기
model.addAttribute("articleList", articleEntityList);
return "articles/index";
}
※ List 를 사용할 경우, findAll() 메서드가 반환하는 데이터 타입은 Iterable 이기 때문에 오류 메시지가 발생한다.
Iterable, Collection, List 인터페이스의 관계.
따라서 익숙치 않은 Iterable 대신, ArrayList를 사용하기 위해 articleRepository 의 부모인 CurdRepository의 메서드를 오버라이딩 해준다.