Day 32 — 전체 복습 + spring-boot 미리보기: 두뇌에 몸을 입히기 전에
목차 23
지난 시간, 우리는 IDE 'Run' 버튼 뒤에서 묵묵히 일하던 빌드 도구 Gradle 을 처음 만났어요. 그러면서 한 가지 약속을 남겨뒀죠. "다음 시간엔 지금까지 배운 전부를 한 데 엮어서, 오늘 배운 ./gradlew run 으로 직접 실행해보자" 고요.
오늘이 바로 그날이에요. 그리고 동시에, java-basic 필수 과정의 마지막 날이기도 해요.
Day 1, 우리는 화면에 "Hello World" 한 줄을 띄우는 것조차 낯설었어요. 변수가 뭔지, 반복문이 뭔지 하나도 몰랐죠. 그런데 31일이 지난 지금, 우리는 클래스를 설계하고, 컬렉션으로 데이터를 다루고, 예외를 체계적으로 처리하고, 람다와 Stream 으로 코드를 선언적으로 짜고, 빌드 도구로 프로젝트를 직접 빌드할 수 있게 됐어요. 정말 먼 길을 왔어요.
오늘은 그 긴 여정을 네 단계로 차근차근 되짚어요. 그리고 그 모든 조각을 한 프로그램에 모아 실제로 돌려본 뒤, 다음 과목인 spring-boot 로 가는 다리를 놓을 거예요. 새로운 걸 배우는 날이 아니라, 지금까지 배운 걸 내 것으로 단단히 굳히는 날이에요. 가볍게 따라와요.
Day 1 ─────────────────────────────────────────────────▶ Day 32
│ │
"Hello World 한 줄도 "30일간 만든 조각을 엮어
낯설던 나" ./gradlew run 으로 실행하는 나"
🎯 학습 목표
- 필수 과정(Day 1~31)을 문법 기초 / 객체지향 / 데이터·예외 / 모던 자바 네 단계로 되짚으며 큰 그림을 잡아요.
- 흩어져 있던 개념들(record·Stream·Optional·컬렉션·예외·Enum·람다)이 실제로 한 프로그램에서 어떻게 맞물리는지 봐요.
- 지난 시간에 예고한
./gradlew run으로 우리가 만든 통합 프로그램을 직접 실행해요. - 우리가 순수 자바로 만든 "두뇌"가, 다음 과목 spring-boot 에서 어떻게 "몸"을 입게 되는지 미리 그려봐요.
오늘의 로드맵
- Step 1 — 다시 만나는 문법 기초: 변수에서 메서드까지 (Day 1~7)
- Step 2 — 객체로 세상을 모델링하다 (Day 8~16)
- Step 3 — 데이터를 다루고 에러를 길들이다 (Day 17~24)
- Step 4 — 짧고 안전한 모던 자바 (Day 25~30)
- Step 5 — 통합 캡스톤: 한 데 엮어
./gradlew run - Step 6 — 두뇌에 몸을 입히러: spring-boot 로 가는 다리 + 필수 과정 완주
Step 1: 다시 만나는 문법 기초 — 변수에서 메서드까지
첫 번째 단계는 Phase 1, 그러니까 Day 1 부터 7 까지예요. 프로그래밍이 뭔지도 모르던 우리가 처음으로 컴퓨터에게 일을 시키는 법을 배운 구간이죠. 핵심을 한 줄기로 다시 꿰어볼게요.
데이터를 담는다 → 흐름을 제어한다 → 기능을 분리한다. 이 세 단계가 Phase 1 의 전부였어요.
[담기] [제어하기] [분리하기]
변수·자료형 → 조건문·반복문 → 메서드
배열 (if / for / while) (재사용 가능한 기능 묶음)
│ │ │
"데이터를 "상황에 따라 "같은 일을 한 번만
어디에 둘까" 다르게, 반복해서" 적어두고 부르자"
먼저 변수와 자료형(Day 2). 데이터를 담는 그릇이에요. 정수는 int, 소수는 double, 참/거짓은 boolean, 글자는 String 에 담았죠.
int likeCount = 320;
String username = "jaehoon";
boolean isPublic = true;
그다음 조건문과 반복문(Day 3~4). 담아둔 데이터를 보고 상황에 따라 다르게 움직이거나, 같은 일을 여러 번 반복하게 했어요.
if (likeCount >= 100) {
System.out.println("인기 게시물!");
}
for (int i = 1; i <= 3; i++) {
System.out.println(i + "번째 게시물");
}
배열(Day 5)은 같은 종류의 데이터를 한 줄로 묶는 그릇이었고요. 메서드(Day 6)는 반복되는 일을 한 번만 적어두고 이름으로 불러 쓰는 "기능 묶음" 이었어요. Day 7 에서는 이 넷을 모아 첫 콘솔 프로그램을 만들었죠.
💡 Phase 1 을 한 문장으로: "데이터를 변수·배열에 담고, 조건문·반복문으로 흐름을 제어하고, 메서드로 기능을 나눈다." 이게 모든 프로그램의 가장 작은 뼈대예요. 오늘 만들 캡스톤도 결국 이 위에 서 있어요.
Step 2: 객체로 세상을 모델링하다
두 번째 단계는 Phase 2, Day 8 부터 16 까지예요. 여기서 우리는 가장 큰 사고의 전환을 겪었어요. "데이터와 행동을 하나로 묶는다" 는 객체지향이죠. spring-boot 의 모든 기반이 여기서 시작돼요.
Phase 1 까지는 데이터(변수)와 기능(메서드)이 따로 놀았어요. 객체지향은 이 둘을 하나의 객체 안에 함께 담았어요.
[절차적 — Phase 1] [객체지향 — Phase 2]
데이터 기능 ┌─────────────────┐
username printPost() │ Member │ ← 데이터(필드)와
email ... │ - username │ 행동(메서드)이
(따로 떨어져 있음) │ - email │ 한 객체 안에 함께
│ + introduce() │
└─────────────────┘
핵심을 순서대로 되짚어요.
- 클래스와 객체(Day 8~9) — 클래스는 붕어빵 틀(설계도), 객체는 그 틀로 찍어낸 붕어빵(실체)이었어요.
new로 객체를 만들고, 필드에 데이터를 담고, 메서드로 행동을 줬죠. - 상속(Day 10) —
extends로 부모의 필드·메서드를 자식이 물려받았어요. 중복을 줄이는 도구였죠. - 다형성(Day 11) — 같은 타입으로 받아도 실제 객체에 따라 다르게 동작하는 성질. 코드를 유연하게 만들었어요.
- 추상 클래스와 인터페이스(Day 12~13) — "뼈대만 정하고 세부는 자식에게"(추상 클래스), "역할을 계약으로 정의"(인터페이스).
- 캡슐화(Day 14) — 필드를
private으로 숨기고 필요한 통로만 여는 정보 은닉. 불변 객체 설계도 배웠죠. - Enum(Day 15) — 정해진 값들의 집합. 게시물 상태 같은 걸 안전하게 표현했어요.
그리고 Day 16 에서, 이 모든 걸 모아 인스타그램의 핵심 도메인을 순수 자바 객체로 설계했어요. 오늘 캡스톤에서 다시 만날 친구들이에요.
// 우리가 Day 8~16에 걸쳐 만든 도메인 객체들 (모양만 떠올려봐요)
Member jaehoon = new Member("jaehoon", "jaehoon@example.com");
Post post = new Post("한강 러닝 5km", jaehoon, 320);
post.setStatus(PostStatus.PUBLIC); // PostStatus 는 Day 15의 Enum
💡 Phase 2 를 한 문장으로: "데이터와 행동을 객체로 묶고, 상속·다형성·인터페이스로 유연하게 설계한다." 인스타그램의
Member·Post·PostStatus가 모두 이 단계의 산물이에요.
Step 3: 데이터를 다루고 에러를 길들이다
세 번째 단계는 Phase 3, Day 17 부터 24 까지예요. 실전에서 매일 쓰는 핵심 라이브러리 구간이죠. 데이터를 효율적으로 담고, 에러를 체계적으로 처리하는 법을 배웠어요.
먼저 String 과 컬렉션. 배열 하나로는 부족했던 자리를 컬렉션이 채워줬어요.
[배열의 한계] [컬렉션 — Day 18~19]
크기가 고정 List — 순서 있는 목록 (중복 OK)
중복 제거 못함 → Set — 중복 없는 모음
키로 못 찾음 Map — 키 → 값 사전
- String 과 래퍼 클래스(Day 17) — 문자열 불변성,
==vsequals,Integer같은 래퍼와 오토박싱. - 컬렉션(Day 18~19) —
List(순서 있는 목록),Set(중복 제거),Map(키-값 사전). 배열보다 훨씬 자유롭게 데이터를 다뤘죠. - 제네릭(Day 20) —
List<String>처럼 "어떤 타입을 담을지" 를 못 박아 타입 안전성을 얻었어요.
그다음 예외 삼총사(Day 21~23). 에러를 무시하지 않고 정면으로 다루는 법이었어요.
예외 잡기(Day 21) → 던지고 전파(Day 22) → 직접 설계(Day 23)
try-catch-finally throw / throws 커스텀 예외 클래스
"터진 걸 받아낸다" "위로 떠넘긴다" "내 상황에 맞는 예외"
Day 24 에서는 이 모든 걸 모아 인스타그램 서비스 계층을 만들었어요. Map·List 기반 인메모리 저장소(MemberRepository·PostRepository)에 회원·게시물을 저장하고, 없는 회원을 찾으면 우리가 직접 만든 MemberNotFoundException 을 던지게 했죠.
// Day 24에서 만든 저장소와 커스텀 예외 (오늘 캡스톤에서 다시 써요)
MemberRepository memberRepository = new MemberRepository();
Long id = memberRepository.save(jaehoon); // 저장하면 번호표(id) 발급
Member found = memberRepository.findById(id); // 없으면 MemberNotFoundException
💡 Phase 3 을 한 문장으로: "컬렉션과 제네릭으로 데이터를 안전하게 다루고, 커스텀 예외로 에러를 체계적으로 처리한다." 이게 진짜 서비스의 살과 뼈예요.
Step 4: 짧고 안전한 모던 자바
네 번째 단계는 Phase 4 의 앞부분, Day 25 부터 30 까지예요. JDK 14~25 에 추가된 모던 문법으로, 같은 일을 더 짧고 더 안전하게 쓰는 법을 배웠어요. (마지막 조각인 Gradle 은 잠시 뒤 Step 5 에서 자연스럽게 다시 만나요.)
핵심은 "어떻게 할지(how) 를 일일이 적던 코드" 가 "무엇을 원하는지(what) 만 적는 코드" 로 바뀐 거예요.
- 람다와 함수형 인터페이스(Day 25) — 익명 클래스의 번거로움을 화살표 한 방(
->)으로 줄였어요. - Stream(Day 26~27) — 컬렉션을 데이터 파이프라인처럼 흘려보내며 거르고(
filter), 바꾸고(map), 모았어요(collect). - Optional(Day 28) —
null을 상자로 감싸NullPointerException을 예방했어요. - Record 와 Sealed(Day 29) — 데이터 클래스의 긴 boilerplate 를
record한 줄로 줄였죠. - 최신 문법(Day 30) — 텍스트 블록·가드 패턴·이름 없는 변수 같은 편의 문법들.
같은 일을 옛날 방식과 모던 방식으로 나란히 보면 차이가 확 와닿아요.
// 옛날 방식 — for 문으로 직접 돌면서 거른다 (how 를 일일이)
List<Post> result = new ArrayList<>();
for (Post post : posts) {
if (post.getStatus() == PostStatus.PUBLIC) {
result.add(post);
}
}
// 모던 방식 — Stream + 람다로 "무엇을 원하는지"만 (what 만)
List<Post> result = posts.stream()
.filter(post -> post.getStatus() == PostStatus.PUBLIC)
.toList();
두 코드는 똑같은 결과를 내요. 하지만 아래쪽이 "공개 게시물만 걸러줘" 라는 의도를 훨씬 또렷하게 보여주죠. 오늘 캡스톤에서 이 모던 문법들이 한꺼번에 쏟아져요.
💡 Phase 4 를 한 문장으로: "람다·Stream·Optional·record 로 코드를 선언적으로(무엇을 원하는지 중심으로) 짧고 안전하게 쓴다."
Step 5: 통합 캡스톤 — 한 데 엮어 ./gradlew run
자, 이제 오늘의 하이라이트예요. 네 단계로 되짚은 조각들을 하나의 프로그램에 모아 실제로 돌려볼 차례예요. 지난 시간에 약속한 ./gradlew run 이 드디어 등장해요.
우리가 만들 프로그램은 Day32Review 라는 작은 통합 데모예요. 30일 동안 만든 "두뇌"(도메인·저장소·예외)를 꺼내, 모던 문법으로 엮어 피드를 요약해주는 프로그램이죠. 어떤 조각이 어디서 쓰이는지 먼저 지도를 그려볼게요.
Day32Review (통합 캡스톤)
│
┌─────────────┬───────────────┬──────────────┬─────────────┐
record Stream Optional 컬렉션 커스텀 예외
(Day 29) (Day 26~27) (Day 28) (Day 18~19) (Day 21~23)
FeedSummary filter/map orElseThrow List / Map MemberNotFound…
groupingBy + Enum / 람다
5-1. 먼저 빌드 도구에게 "실행" 을 부탁하기 — application 플러그인
지난 시간 우리 build.gradle.kts 에는 java 플러그인만 있었어요. 그건 "이건 자바 프로젝트야" 라는 선언이었죠. 그런데 ./gradlew run 으로 프로그램을 실행하려면, Gradle 에게 "어떤 클래스의 main 을 실행할지" 를 알려줘야 해요. 그 일을 해주는 게 application 플러그인이에요.
// build.gradle.kts
plugins {
java
application // ← 추가: "이 프로젝트는 실행 가능한 프로그램이다"
}
application {
mainClass = "com.instagram.javabasic.Day32Review" // ← 실행 시작점
}
application 플러그인을 더하고 mainClass 에 시작점 클래스를 적어두면, 이제 ./gradlew run 한 줄로 그 클래스의 main 이 실행돼요. 빌드 도구가 컴파일부터 실행까지 알아서 챙겨주는 거죠.
5-2. 조각 하나하나를 메서드로 — 모던 문법이 쏟아진다
이제 통합 로직을 작은 메서드들로 나눠 만들어요. 각 메서드가 어떤 Day 의 개념을 쓰는지 주석으로 표시했어요. 먼저 공개 게시물만 좋아요 순으로 거르기 — Stream·Enum·람다가 한자리에 모여요.
// com/instagram/javabasic/Day32Review.java
// (전체 코드: src/main/java/com/instagram/javabasic/Day32Review.java)
// Day 26~27 의 Stream + Day 15 의 Enum + Day 25 의 람다.
// 공개(PUBLIC) 상태인 게시물만 람다 조건으로 걸러, 좋아요 많은 순으로 정렬해 돌려줘요.
public static List<Post> publicPostsByLikes(List<Post> posts) {
return posts.stream()
.filter(post -> post.getStatus() == PostStatus.PUBLIC)
.sorted(Comparator.comparingInt(Post::getLikeCount).reversed())
.toList();
}
다음은 작성자별로 게시물 묶기 — Stream 의 groupingBy 와 Map 이 만나요.
// Day 26~27 의 Stream groupingBy + Day 18~19 의 Map.
// 작성자 이름별로 게시물을 모아 "이름 → 글 묶음" 지도를 만들어요.
public static Map<String, List<Post>> groupByAuthor(List<Post> posts) {
return posts.stream()
.collect(Collectors.groupingBy(Post::getAuthorName));
}
그리고 회원을 찾되 없으면 예외 — Optional 과 커스텀 예외가 짝을 이뤄요.
// Day 28 의 Optional + Day 21~23 의 커스텀 예외.
// 저장소에서 회원을 찾되, 없으면 우리가 만든 MemberNotFoundException 을 던져요.
public static Member requireMember(MemberRepository repository, Long id) {
return Optional.ofNullable(findOrNull(repository, id))
.orElseThrow(() -> new MemberNotFoundException("id " + id + " 회원이 없어 요약을 만들 수 없어요."));
}
마지막으로 요약을 record 로 묶기 — Day 29 의 record 가 결과를 불변 DTO 한 줄로 담아요.
// Day 29 의 record — 피드 화면에 보여줄 요약 한 줄을 불변 DTO 로 묶어요.
// 필드 3개를 적으면 생성자·접근자·toString·equals 가 한 줄로 다 만들어져요.
public record FeedSummary(String username, int postCount, int totalLikes) {
}
5-3. main 이 조각들을 엮는다
이제 main 에서 이 조각들을 순서대로 불러 하나의 흐름을 만들어요. 저장소에 회원·게시물을 넣고, 위 메서드들로 가공해 화면에 뿌리는 거예요.
// com/instagram/javabasic/Day32Review.java (main 발췌)
public static void main(String[] args) {
// 우리가 30일 동안 만든 저장소를 그대로 꺼내 써요 — 인메모리 사물함이에요.
MemberRepository memberRepository = new MemberRepository();
PostRepository postRepository = new PostRepository();
Member jaehoon = new Member("jaehoon", "jaehoon@example.com");
Member minji = new Member("minji", "minji@example.com");
Long jaehoonId = memberRepository.save(jaehoon);
memberRepository.save(minji);
Post p1 = new Post("한강 러닝 5km", jaehoon, 320);
Post secret = new Post("비밀 메모", jaehoon, 999);
secret.setStatus(PostStatus.PRIVATE); // Day 15 Enum — 비공개로
// ... (게시물 저장)
// 1) 공개 게시물만 좋아요 순 — Stream + Enum + 람다
// 2) 작성자별 묶음 — groupingBy + Map
// 3) 작성자 요약 — record + Optional + 커스텀 예외
// 4) 없는 회원 조회 — try-catch 로 커스텀 예외 받기
}
네 가지 동작이 한 흐름에 모여요. 30일 동안 따로따로 배운 개념들이, 이렇게 한 프로그램 안에서 자연스럽게 맞물리는 거죠.
5-4. 드디어 실행 — ./gradlew run
이제 터미널에 지난 시간 예고했던 그 명령을 입력해요.
$ ./gradlew run
그러면 Gradle 이 컴파일하고, Day32Review 의 main 을 실행해서 이런 결과를 보여줘요.
=== 공개 게시물 (좋아요 많은 순) ===
jaehoon | 한강 러닝 5km | 좋아요 320
minji | 주말 산책 | 좋아요 150
jaehoon | 오늘의 카페 | 좋아요 88
=== 작성자별 게시물 수 ===
minji: 1개
jaehoon: 3개
=== 피드 요약 (record) ===
FeedSummary[username=jaehoon, postCount=3, totalLikes=1407]
=== 없는 회원 조회 (예외 처리) ===
예외를 잡았어요: id 999 회원이 없어 요약을 만들 수 없어요.
찬찬히 읽어볼까요? 첫 줄에서 비공개 게시물(좋아요 999짜리 '비밀 메모')은 빠지고 공개 게시물만 좋아요 순으로 정렬됐어요. Stream·Enum·람다가 함께 일한 결과죠. 둘째 묶음은 작성자별 글 수인데, groupingBy 가 모아준 거예요. 셋째 FeedSummary[...] 형태는 record 가 만든 불변 요약이고요. 마지막엔 없는 회원(id 999)을 찾자 우리가 만든 예외가 흐름을 막고 메시지를 보여줬어요.
30일 동안 만든 "두뇌" 가, 빌드 도구의 run 명령 한 줄로 실제로 살아 움직이는 순간이에요. 이 통합 동작은 코드베이스에서 다섯 가지 케이스로 검증되어 있어, 손으로 직접 돌려봐도 똑같이 나와요.
💡 캡스톤이 보여주는 것: 개념은 따로 배우지만, 실전에선 함께 쓰인다. record 따로, Stream 따로가 아니라 — 하나의 작은 기능 안에서 자연스럽게 맞물리는 게 진짜 프로그래밍이에요.
Step 6: 두뇌에 몸을 입히러 — spring-boot 로 가는 다리 + 필수 과정 완주
마지막 Step 이에요. 우리가 32일 동안 만든 게 정확히 뭐였는지, 그리고 그게 다음 과목에서 어떻게 자라는지 그려볼게요.
우리는 순수 자바로 인스타그램의 "두뇌" 를 만들었어요. 회원·게시물 같은 도메인 객체, 그걸 담는 저장소, 에러를 다루는 예외 — 데이터를 어떻게 다룰지에 대한 생각의 구조죠. 그런데 이 두뇌엔 아직 "몸" 이 없어요. 바깥세상(브라우저·앱)과 대화할 입과 손, 데이터를 영원히 보관할 기억 장치가 없죠.
[ java-basic 에서 만든 것 — "두뇌" (생각의 구조) ]
· 도메인 객체 · 인메모리 저장소 · 커스텀 예외
│
│ spring-boot 에서 "몸" 을 입으면
▼
[ 살아 움직이는 서비스 — "두뇌 + 몸" ]
· 웹으로 요청 받기 · 진짜 DB에 저장 · 자동 에러 응답
다음 과목 spring-boot 에서는, 우리가 만든 두뇌의 각 부분이 프레임워크의 힘을 빌려 한 단계씩 자라요. "다음 과목에서 배울 것" 으로 한 줄씩만 미리 맛볼게요. (지금은 이름만 슬쩍 — 자세한 건 spring-boot Day 1 부터예요.)
| 우리가 순수 자바로 만든 것 | spring-boot 에서 자라는 모습 (다음 과목 예고) |
|---|---|
| 클래스 설계, 생성자로 객체 넘기기 (Day 8·24) | 객체를 외부에서 주입받는 방식 — DI(의존성 주입) |
Member·Post 도메인 객체 (Day 16) |
데이터베이스 테이블과 객체를 잇는 엔티티(entity) |
record 불변 DTO (Day 29) |
요청·응답을 실어 나르는 Request/Response DTO |
| 컬렉션 기반 인메모리 저장소 (Day 24) | 진짜 DB 와 연결되는 Repository |
| 커스텀 예외 계층 (Day 21~23) | 모든 예외를 한곳에서 처리하는 @ExceptionHandler |
제네릭 <T> (Day 20) |
응답을 감싸는 ApiResponse<T> |
build.gradle.kts (Day 31) |
의존성 한 줄로 웹 서버를 띄우는 설정 |
보이시나요? 오늘 우리가 캡스톤에서 쓴 그 Member·Post·record·커스텀 예외가, 다음 과목에서 하나도 버려지지 않고 그대로 자라요. java-basic 에서 만든 두뇌가 탄탄할수록 spring-boot 가 훨씬 쉬워지는 이유예요.
그리고, 필수 과정 완주를 축하해요 🎯
Day 1 의 "Hello World" 부터 오늘 ./gradlew run 까지, 정말 긴 여정이었어요. 변수 하나 담는 것도 어색하던 우리가, 이제 객체를 설계하고 데이터를 다루고 에러를 처리하고 빌드 도구로 프로그램을 실행할 수 있게 됐어요. 이건 결코 작은 일이 아니에요.
기억해두세요. 오늘 복습한 네 단계 — 문법 기초 → 객체지향 → 데이터·예외 → 모던 자바 — 이 흐름이 앞으로 어떤 언어, 어떤 프레임워크를 배우든 똑같이 반복돼요. 우리는 그 단단한 토대를 만든 거예요.
💡 필수 과정의 마지막 한 줄: java-basic 은 "두뇌" 를 만드는 과정이었고, spring-boot 는 그 두뇌에 "몸" 을 입히는 과정이에요. 두뇌가 탄탄하면 몸은 빨리 자라요.
마무리
오늘은 java-basic 필수 과정의 마지막 날로, 새 개념을 배우기보다 32일의 여정을 한 데 모아 굳히는 시간이었어요.
- 네 단계 복습 — 문법 기초(변수~메서드) · 객체지향(클래스~Enum) · 데이터·예외(컬렉션~서비스 계층) · 모던 자바(람다~record). 흩어진 개념을 큰 그림으로 다시 꿰었어요.
- 통합 캡스톤 —
Day32Review하나에 record·Stream·Optional·컬렉션·예외·Enum·람다를 모두 엮어,application플러그인을 더하고 지난 시간 예고한./gradlew run으로 직접 실행했어요. - spring-boot 다리 — 순수 자바로 만든 "두뇌" 가 다음 과목에서 어떻게 "몸" 을 입는지 미리 그렸어요. 오늘 쓴 도메인·record·예외가 그대로 자라요.
다음 시간엔 — 두뇌에 몸을 입히는 첫걸음
다음 과목은 spring-boot 예요. 오늘 미리 본 그 다리를 실제로 건너요. 우리가 만든 Member·Post 가 진짜 데이터베이스와 연결되고, 인메모리 저장소가 진짜 Repository 로 바뀌고, 브라우저에서 보낸 요청을 받아 응답하는 — 살아 움직이는 웹 서비스를 만들기 시작해요. java-basic 에서 쌓은 토대가 거기서 빛을 발할 거예요. 정말 수고 많으셨어요. 다음 과목에서 만나요!
과제
오늘 과제는 코드를 처음부터 새로 짜기보다, 우리가 만든 캡스톤을 직접 돌려보고 한 군데씩 손봐가며 복습한 개념이 손에 익었는지 확인하는 흐름이에요. IntelliJ 로 instagram-java-basic 프로젝트를 연 상태에서 풀어봐요.
과제 1: 캡스톤 직접 실행하고 데이터 추가하기
상황 배경: Step 5 에서 ./gradlew run 으로 실행한 그 프로그램을, 이번엔 직접 돌리고 데이터를 한 명 더 넣어봐요.
🎯 해결 미션:
- 터미널에서
./gradlew run을 실행하고, Step 5 에서 본 것과 같은 출력이 나오는지 확인하세요. Day32Review의main에 회원 한 명(예:"seungwoo")과 그 회원의 공개 게시물 두 개를 추가하세요. 좋아요 수는 자유롭게 정해요.- 다시
./gradlew run을 실행해서, 공개 게시물 정렬과 작성자별 게시물 수에 새 회원이 어떻게 끼어드는지 출력으로 확인하세요.
새 데이터를 넣었을 때 좋아요 순 정렬이 어디에 끼어드는지를 눈으로 보는 게 목적이에요.
과제 2: 조각 하나 갈아 끼우기 — 정렬 뒤집기
상황 배경: publicPostsByLikes 메서드는 공개 게시물을 좋아요 많은 순으로 정렬해요. 이걸 반대로 바꿔봐요.
🎯 해결 미션:
publicPostsByLikes안의 정렬 부분을 손봐서, 좋아요 적은 순(오름차순)으로 정렬되게 바꿔보세요. (힌트:.reversed()가 무슨 일을 하고 있었는지 떠올려보세요.)./gradlew run을 실행해서 정렬 순서가 뒤집혔는지 확인하세요.- 확인했으면 원래대로(내림차순) 되돌려두세요.
Day 25 의 람다와 Day 27 의 정렬을 손으로 만져보며 "내가 정렬 방향을 직접 정할 수 있구나" 를 체감하는 게 목적이에요.
과제 3: 32일 회고 — 내 언어로 한 문장 설명하기
상황 배경: 마지막 과제는 코드가 아니라 회고예요. 배운 걸 내 언어로 설명할 수 있어야 진짜 내 것이에요.
🎯 해결 미션: 오늘 복습한 네 단계(문법 기초 / 객체지향 / 데이터·예외 / 모던 자바) 중에서,
- 가장 어려웠던 개념 하나를 고르고, 왜 어려웠는지 한 줄 적어보세요.
- 그 개념을 남에게 설명하듯 한 문장으로 풀어 써보세요. (예: "인터페이스는 '이런 기능을 갖춰라' 라고 정해두는 계약서 같은 거예요.")
- 그 개념이 오늘 캡스톤 코드 어디에서 쓰였는지 한 군데 찾아 짚어보세요.
정답은 없어요. 내 언어로 설명할 수 있는 개념이 진짜 아는 개념이라는 걸 확인하는 과제예요.
생각해볼 주제
혼자 고민해도 좋고, 동료와 토론해도 좋아요. 정답을 외우기보다 "나라면 어떻게 설명할까" 를 떠올리며 읽어보세요.
1. 순수 자바로도 잘 도는데, 왜 굳이 spring-boot 라는 "몸" 이 필요할까?
오늘 만든 Day32Review 는 spring-boot 없이도 잘 돌았어요. 회원을 저장하고, 게시물을 거르고, 요약을 만들고 — 필요한 일을 다 했죠.
그런데도 실무에서는 거의 다 spring-boot 같은 프레임워크 위에서 서비스를 만들어요. "혼자 콘솔에서 도는 프로그램" 과 "수많은 사용자가 브라우저로 접속하는 서비스" 사이에는 어떤 차이가 있을까요? 웹 요청을 받고, 여러 명이 동시에 쓰고, 데이터를 영원히 보관하는 일을 전부 직접 만든다면 얼마나 번거로울지 떠올려보세요.
2. 프로그램을 끄면 데이터가 사라진다 — 진짜 서비스는 어떻게 기억할까?
우리가 만든 MemberRepository 는 데이터를 Map 에 담아요. 편하긴 한데, 한 가지 큰 약점이 있어요. 프로그램을 끄면 Map 안의 데이터가 전부 사라진다는 거예요. 다시 켜면 회원도 게시물도 0 부터 시작이죠.
실제 인스타그램은 앱을 껐다 켜도 내 게시물이 그대로 남아 있어요. 그렇다면 진짜 서비스는 데이터를 어디에, 어떻게 보관하길래 꺼져도 안 사라질까요? "메모리(Map)" 와 "영원히 남는 저장소" 의 차이가 무엇일지 생각해보세요. (다음 과목에서 만날 데이터베이스의 존재 이유예요.)
3. 같은 기능을 옛날 for 문으로도, 모던 Stream 으로도 짤 수 있다면 — 뭘 기준으로 고를까?
Step 4 에서 공개 게시물을 거르는 코드를 두 가지로 봤어요. 옛날 for 문 방식과 모던 Stream 방식. 둘은 똑같은 결과를 내요.
그렇다면 실무에서 코드를 짤 때 무엇을 기준으로 둘 중 하나를 고를까요? "더 짧은 코드", "더 읽기 쉬운 코드", "팀원들이 더 익숙한 코드" — 이 중 무엇이 가장 중요할까요? 그리고 "짧다" 와 "읽기 쉽다" 가 항상 같은 말일까요? 정답이 하나가 아닌 이 질문을, 내 기준을 세워가며 고민해보세요.
✅ 예시 답안정답 보기
오늘 과제는 우리가 만든 통합 캡스톤 Day32Review 를 직접 돌려보고 한 군데씩 손봐가며 복습한 개념이 손에 익었는지 확인하는 흐름이에요. 과제 1 은 ./gradlew run 으로 실행하고 데이터를 추가해보고, 과제 2 는 정렬 방향을 뒤집어보고, 과제 3 은 배운 걸 내 언어로 설명해봐요. 교안 Step 4~5 를 옆에 두고 비교하면서 보면 편해요.
과제 예시답안
과제 1 예시답안 — 캡스톤 직접 실행하고 데이터 추가하기
핵심 접근
이미 완성된 Day32Review 를 돌려보고, main 에 회원·게시물을 더 넣었을 때 출력이 어떻게 달라지는지 눈으로 확인하는 과제예요. 새 데이터를 만드는 방법은 교안 Step 5-3 의 main 코드와 똑같아요 — new Member(...), new Post(...) 를 한 번 더 쓰면 돼요.
예상 결과
main 에 회원 seungwoo 와 공개 게시물 두 개를 추가하면 이런 모양이에요.
Member seungwoo = new Member("seungwoo", "seungwoo@example.com");
memberRepository.save(seungwoo);
Post p4 = new Post("제주 여행", seungwoo, 210);
Post p5 = new Post("강아지 산책", seungwoo, 95);
postRepository.save(p4);
postRepository.save(p5);
그리고 통합 목록(all)에도 새 게시물을 함께 넣어줘야 결과에 반영돼요. 다시 ./gradlew run 을 실행하면, 좋아요 순 정렬에 seungwoo 의 글(210·95)이 끼어들고, 작성자별 게시물 수에도 seungwoo: 2개 가 새로 나타나요.
=== 공개 게시물 (좋아요 많은 순) ===
jaehoon | 한강 러닝 5km | 좋아요 320
seungwoo | 제주 여행 | 좋아요 210 ← 새로 끼어든 자리
minji | 주말 산책 | 좋아요 150
seungwoo | 강아지 산책 | 좋아요 95
jaehoon | 오늘의 카페 | 좋아요 88
좋아요 210 짜리 글이 320 과 150 사이에 정확히 끼어든 게 보이죠. Stream 의 정렬이 새 데이터까지 알아서 자리를 잡아준 거예요.
채점 포인트
| 확인할 점 | 왜 중요한가 |
|---|---|
./gradlew run 으로 실제 실행했는가 |
빌드 도구로 프로그램을 직접 돌리는 게 오늘의 핵심 경험이에요 |
new Member · new Post 로 데이터를 추가했는가 |
Day 8·16의 객체 생성을 손으로 다시 해보는 게 목적이에요 |
| 통합 목록에도 새 게시물을 넣어 결과에 반영했는가 | 데이터를 만들기만 하고 목록에 안 넣으면 출력이 안 바뀌어요 |
| 새 글이 좋아요 순 어디에 끼어드는지 확인했는가 | 정렬이 데이터에 따라 자동으로 동작함을 체감하는 게 핵심이에요 |
흔한 실수
- 게시물을
new Post(...)로 만들기만 하고 통합 목록(all)에 안 넣어요. 그러면 저장은 됐어도 화면 출력엔 안 나타나요. 만든 게시물을 목록에도 함께 넣어야 해요. - 좋아요 수를 모두 같게 줘서 정렬 차이가 안 보여요. 일부러 서로 다른 값을 줘야 정렬이 어떻게 끼어드는지 보여요.
- 비공개로 만든 글이 왜 안 보이냐고 당황해요.
publicPostsByLikes는 공개 게시물만 거르기 때문에,setStatus(PostStatus.PRIVATE)한 글은 정렬 목록에서 빠지는 게 정상이에요.
과제 2 예시답안 — 조각 하나 갈아 끼우기: 정렬 뒤집기
핵심 접근
publicPostsByLikes 의 정렬을 뒤집는 과제예요. 핵심은 .reversed() 가 무슨 일을 하고 있었는지 떠올리는 거예요. Comparator.comparingInt(Post::getLikeCount) 는 기본이 오름차순(작은 것 먼저)이고, 거기에 .reversed() 를 붙여서 내림차순(큰 것 먼저)으로 뒤집고 있었어요. 그러니 .reversed() 만 빼면 오름차순이 돼요.
예상 결과
// Before — 좋아요 많은 순 (내림차순)
.sorted(Comparator.comparingInt(Post::getLikeCount).reversed())
// After — 좋아요 적은 순 (오름차순): .reversed() 제거
.sorted(Comparator.comparingInt(Post::getLikeCount))
.reversed() 를 빼고 ./gradlew run 을 실행하면 순서가 정확히 뒤집혀요.
=== 공개 게시물 (좋아요 많은 순) ===
jaehoon | 오늘의 카페 | 좋아요 88 ← 이제 가장 적은 게 위로
minji | 주말 산책 | 좋아요 150
jaehoon | 한강 러닝 5km | 좋아요 320
확인했으면 .reversed() 를 다시 붙여 원래대로(내림차순) 되돌려두면 돼요.
채점 포인트
| 확인할 점 | 왜 중요한가 |
|---|---|
.reversed() 가 정렬을 뒤집는 역할임을 파악했는가 |
정렬 방향이 어디서 정해지는지 짚는 게 목적이에요 |
.reversed() 제거로 오름차순이 되는 걸 확인했는가 |
"내가 정렬 방향을 직접 정한다" 를 손으로 체감하는 게 핵심이에요 |
| 확인 후 원래대로 되돌렸는가 | 실험한 코드는 원상복구하는 습관도 함께 익혀요 |
흔한 실수
- 정렬을 뒤집겠다고 데이터(좋아요 수)를 직접 고쳐요. 데이터는 그대로 두고 정렬 코드만 바꾸는 게 과제예요.
.reversed()를 빼는 대신 다른 줄을 건드려 컴파일 에러가 나요. 딱.reversed()한 조각만 떼면 돼요. 에러가 나면 메시지를 차분히 읽어보세요 — Day 21에서 배운 대로 에러는 친구예요.
과제 3 예시답안 — 32일 회고: 내 언어로 한 문장 설명하기
핵심 접근
정답이 없는 회고 과제예요. 중요한 건 어떤 개념을 골랐느냐가 아니라, 그 개념을 남이 알아듣게 내 말로 풀 수 있느냐예요. 교과서 정의를 그대로 옮기는 게 아니라, 비유나 예시를 곁들여 설명할 수 있으면 진짜 아는 거예요.
예시 회고
아래는 하나의 예시예요. 정답이 아니라 "이런 식으로 적으면 돼요" 의 본보기예요.
- 가장 어려웠던 개념: 인터페이스(Day 13). 추상 클래스랑 뭐가 다른지, 왜 굳이 "역할만 정하는" 게 필요한지 처음엔 와닿지 않았어요.
- 내 언어로 한 문장: "인터페이스는 '이런 기능은 꼭 갖춰라' 라고 정해두는 계약서 같은 거예요. 어떻게 구현할지는 각자 자유지만, 정해진 기능은 반드시 만들어야 해요."
- 캡스톤 어디에 쓰였나: 오늘 캡스톤에서 직접 쓰진 않았지만,
List가 바로 인터페이스예요.groupByAuthor가 돌려주는Map<String, List<Post>>의List는 "순서대로 담는 역할" 을 정한 계약서고, 실제 구현은 그 뒤에 숨어 있어요.
채점 포인트
| 확인할 점 | 왜 중요한가 |
|---|---|
| 어려웠던 개념을 솔직하게 골랐는가 | 막혔던 지점을 마주하는 게 복습의 출발이에요 |
| 정의를 베끼지 않고 내 말/비유로 풀었는가 | 내 언어로 설명할 수 있어야 진짜 아는 개념이에요 |
| 그 개념이 코드 어디서 쓰였는지 짚었는가 | 개념과 실제 코드를 연결하는 게 핵심이에요 |
흔한 실수
- 교과서 정의를 그대로 옮겨 적어요. "남에게 설명하듯" 이 핵심이라, 내 말로 바꿔보는 게 중요해요.
- "다 쉬웠어요" 라고 넘어가요. 어려웠던 지점을 솔직히 짚어야 그 부분이 진짜 내 것이 돼요.
생각해볼 주제 예시답안
생각해볼 주제 1 예시답안 — 왜 굳이 spring-boot 라는 "몸" 이 필요할까?
[문제 상황 요약]
오늘 만든 Day32Review 는 프레임워크 없이도 잘 돌았어요. 그런데도 실무에서는 거의 다 spring-boot 같은 프레임워크 위에서 서비스를 만들어요. "혼자 콘솔에서 도는 프로그램" 과 "수많은 사용자가 브라우저로 접속하는 서비스" 사이엔 어떤 차이가 있을까요?
[튜터의 가이드 및 해설]
핵심은 "반복되는 뒷일을 직접 만들 것인가, 빌려 쓸 것인가" 예요.
우리 캡스톤은 콘솔에서 한 번 돌고 끝나는 프로그램이에요. 그런데 진짜 인스타그램 같은 서비스는 훨씬 많은 일을 해야 해요. 브라우저가 보낸 요청을 받아 해석하고, 수천 명이 동시에 접속해도 안 꼬이게 처리하고, 데이터를 영원히 보관하고, 에러가 나면 적절한 응답을 돌려주고… 이런 "서비스라면 당연히 갖춰야 하는 뒷일" 이 산더미예요.
이걸 전부 직접 만든다면 어떨까요? 회원 가입 기능 하나 만들기 전에, 웹 요청 받는 코드부터 수천 줄을 짜야 해요. 그것도 프로젝트마다 매번 똑같이요. 프레임워크는 이 "매번 똑같이 반복되는 뒷일" 을 미리 다 만들어둔 도구예요. 우리는 그 위에서 진짜 하고 싶은 일(비즈니스 로직) 에만 집중하면 돼요.
우리가 만든 두뇌(도메인·저장소·예외)는 버려지지 않아요. 오히려 그 두뇌가 프레임워크라는 몸을 만나 손과 발과 기억 장치를 얻는 거예요. java-basic 에서 토대를 탄탄히 쌓았기 때문에, 그 위에 몸을 입히는 spring-boot 가 훨씬 수월해지는 거고요.
🎯 면접관을 홀리는 핵심 멘트
"프레임워크 없이도 프로그램은 돌지만, 웹 요청 처리·동시 접속·데이터 영속화·에러 응답처럼 모든 서비스가 똑같이 필요로 하는 뒷일을 매번 직접 만드는 건 비효율적입니다. 프레임워크는 이 반복되는 인프라를 미리 갖춰둬서, 개발자가 비즈니스 로직에 집중하게 해줍니다. 순수 자바로 만든 도메인 설계가 탄탄할수록 그 위에 프레임워크를 얹는 작업이 수월해진다고 생각합니다."
생각해볼 주제 2 예시답안 — 프로그램을 끄면 데이터가 사라진다, 진짜 서비스는 어떻게 기억할까?
[문제 상황 요약]
우리가 만든 MemberRepository 는 데이터를 Map 에 담아요. 편하지만, 프로그램을 끄면 Map 안의 데이터가 전부 사라져요. 다시 켜면 0 부터 시작이죠. 실제 인스타그램은 앱을 껐다 켜도 게시물이 그대로 남아 있는데, 진짜 서비스는 데이터를 어디에 어떻게 보관할까요?
[튜터의 가이드 및 해설]
핵심은 "메모리는 잠깐, 디스크는 오래" 라는 차이예요.
Map 이나 List 같은 컬렉션은 프로그램이 실행되는 동안 메모리(RAM) 위에 데이터를 올려둬요. 메모리는 빠르지만, 프로그램이 꺼지면(또는 컴퓨터 전원이 나가면) 그 안의 내용이 싹 지워져요. 우리 인메모리 저장소가 꺼지면 데이터가 사라지는 이유가 바로 이거예요.
영원히 남기려면 데이터를 꺼져도 지워지지 않는 곳 — 디스크에 적어둬야 해요. 그 일을 전문으로 하는 게 데이터베이스예요. 데이터베이스는 데이터를 디스크에 안전하게 보관하고, 프로그램이 껐다 켜져도, 심지어 서버가 여러 대여도 같은 데이터를 꺼내 쓸 수 있게 해줘요.
재미있는 건, 우리가 만든 MemberRepository 의 구조가 헛수고가 아니라는 거예요. "저장하고(save), 찾고(findById)" 라는 그 모양 그대로, 다음 과목에서 진짜 데이터베이스와 연결되는 Repository 로 자라요. 우리는 이미 "저장소란 이런 모양이다" 를 손으로 만들어본 거예요. 메모리에 담던 걸 디스크에 담는 것으로 바뀔 뿐이죠.
🎯 면접관을 홀리는 핵심 멘트
"컬렉션 기반 인메모리 저장소는 데이터를 메모리에 올려두기 때문에 프로그램이 종료되면 사라집니다. 영속성이 필요한 실제 서비스는 데이터를 디스크에 보관하는 데이터베이스를 사용해, 재시작이나 서버 증설에도 데이터가 유지되도록 합니다. 인메모리 저장소에서 익힌
save·findById같은 인터페이스 모양은 그대로 둔 채, 데이터를 담는 곳만 메모리에서 데이터베이스로 바뀌는 것이 핵심이라고 이해하고 있습니다."
생각해볼 주제 3 예시답안 — for 문과 Stream, 무엇을 기준으로 고를까?
[문제 상황 요약]
Step 4 에서 공개 게시물을 거르는 코드를 옛날 for 문 방식과 모던 Stream 방식 두 가지로 봤어요. 둘은 똑같은 결과를 내요. 실무에서 코드를 짤 때 무엇을 기준으로 둘 중 하나를 고를까요? 그리고 "짧다" 와 "읽기 쉽다" 가 항상 같은 말일까요?
[튜터의 가이드 및 해설]
핵심은 "코드는 컴퓨터보다 사람이 더 자주 읽는다" 는 거예요.
먼저 분명히 해둘 게 있어요. for 문이든 Stream 이든 결과가 같다면, 어느 쪽이 "틀린" 건 아니에요. 선택의 문제죠. 그럼 기준이 뭐냐 — 보통은 가독성, 즉 "다음에 이 코드를 읽는 사람이 의도를 얼마나 빨리 파악하느냐" 예요.
Stream 은 "공개 게시물만 걸러서 좋아요 순으로" 같은 의도를 또렷하게 드러낼 때 강해요. filter·sorted 라는 이름이 곧 설명이 되니까요. 반대로 단순한 반복이나, 중간에 복잡한 분기가 많은 경우엔 오히려 for 문이 더 읽기 쉬울 때도 있어요. 그러니 "무조건 Stream 이 좋다" 는 건 아니에요.
그리고 "짧다 ≠ 읽기 쉽다" 예요. 한 줄에 너무 많은 걸 욱여넣어 짧게 만들면, 짧지만 해독하기 어려운 코드가 돼요. 진짜 좋은 코드는 짧으면서도 의도가 또렷한 코드예요.
마지막으로 현실적인 기준 하나 — 팀 컨벤션이에요. 혼자 짜는 코드가 아니라면, 우리 팀이 익숙하고 합의한 방식을 따르는 게 "내가 제일 좋아하는 방식" 보다 중요할 때가 많아요. 모두가 같은 스타일로 짜야 서로의 코드를 빨리 읽을 수 있으니까요.
🎯 면접관을 홀리는 핵심 멘트
"for 문과 Stream 중 선택 기준은 가독성이라고 생각합니다. filter·map 처럼 의도가 이름으로 드러나는 경우엔 Stream 이 읽기 좋고, 단순 반복이나 복잡한 분기에는 for 문이 더 명확할 때도 있습니다. 짧은 코드가 항상 읽기 쉬운 건 아니어서, 의도가 또렷하게 드러나는지를 우선합니다. 또 혼자가 아니라 팀으로 일한다면 팀 컨벤션을 따르는 것이 개인 선호보다 중요하다고 봅니다."
