Day 16 — 종합: 인스타그램 도메인 모델 설계 (Phase 2 캡스톤)
안녕하세요! 여러분의 Java 가이드, 홍순구 튜터입니다.
Day 16에 오신 걸 환영해요! 지난 시간 마지막에 제가 이런 말로 마무리했던 거 기억하시나요? "다음 시간엔 지금까지 만든 부품들을 한데 모아서 인스타그램 도메인 모델을 종합 설계해본다. 흩어져 있던 조각들이 하나로 맞춰지는 순간이라 꽤 짜릿할 거다" 라고요. 오늘이 바로 그 시간이에요.
그래서 오늘은 좀 특별해요. 새로 배우는 문법이 0개예요. Day 8부터 우리는 클래스, 상속, 다형성, 추상 클래스, 인터페이스, Enum 을 하나씩 익혀왔죠. 그런데 사실 이것들을 따로따로 배울 때는 "이게 진짜 어디에 쓰이는 거지?" 하는 느낌이 살짝 있었을 거예요. 오늘은 그 조각들을 한데 모아서, 진짜 인스타그램의 "두뇌" 라고 부를 만한 도메인 모델로 엮어봅니다. 새 무기를 얻는 날이 아니라, 그동안 모은 무기들을 진열장에 가지런히 배치하는 날이에요.
오늘 우리가 갈 길을 미리 그려볼게요.
- 먼저 지금까지 만든 조각들을 한 장의 관계도 로 펼쳐봐요. 따로 배운 게 사실은 하나의 큰 그림이었다는 걸 눈으로 확인해요.
- 글의 작성자를 이름표(문자열)가 아니라 실제 회원 객체로 연결 해봐요. 이걸 연관관계라고 불러요.
- 회원이 자기가 쓴 글들을 목록으로 갖게 만들어봐요(1:N 관계).
- 팔로우 관계를 객체로 표현 해봐요. 사람이 사람을 가리키는 구조죠.
- 인터페이스로 공통 행동을 묶어 서 도메인을 더 깔끔하게 만들어봐요.
- 완성된 모델로 간단한 시연 을 돌려보며 조각들이 맞물려 도는 걸 확인해요.
- 마지막으로 Phase 2 전체를 회고 하며, 우리가 무엇을 만들었는지 정리해요.
오늘도 "왜 필요한가 → 어떻게 쓰는가 → 이름이 뭔가" 순서로 가요. 자, 그럼 먼저 우리가 가진 조각들을 책상 위에 쏟아놓고 시작해볼까요?
🎯 학습 목표
- 지금까지 만든 도메인 조각들(
Member·Post·Comment·Enum·인터페이스)이 하나의 큰 그림의 부품이었음을 관계도로 설명할 수 있다 - 연관관계(association) 가 "객체가 다른 객체를 필드로 가리키는 것" 임을 이해한다
- 작성자를 문자열 이름표가 아니라 실제
Member객체 참조로 연결할 때의 장점을 설명할 수 있다 - 회원이 자기 글 목록을 갖는 1:N 관계를 배열로 표현할 수 있다
- 팔로우 관계를 별도 객체로 표현하는 설계의 의미를 이해한다
- 공통 행동을 인터페이스로 묶어 도메인 모델을 정리할 수 있다
Step 1. 인스타그램 도메인을 한 장에 그려보자
자, 오늘 첫걸음은 코드를 짜는 게 아니에요. 책상 정리부터 해요. 우리가 Day 8부터 만들어온 클래스들을 전부 꺼내서 책상 위에 늘어놓고, "이것들이 서로 어떤 사이인지" 를 한 장의 그림으로 그려보는 거예요. 설계라는 건 사실 코드를 짜기 전에 이렇게 그림부터 그리는 일이거든요.
코드는 다음 Step부터 본격적으로 손대요. 이번엔 개념과 그림에만 집중할게요.
우리가 가진 조각들
먼저 그동안 만든 부품들을 쭉 모아볼게요. 하나씩 떠올려보세요.
┌─────────────────────────────────────────────────────┐
│ 지금까지 만든 도메인 조각들 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Member │ │ Post │ │ Comment │ │
│ │ (회원) │ │ (게시물) │ │ (댓글) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 클래스 클래스 클래스 │
│ │
│ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ PostStatus │ │ Shareable · Commentable │ │
│ │ (게시물상태) │ │ (역할 인터페이스) │ │
│ └──────────────┘ └─────────────────────────┘ │
│ Enum 인터페이스 │
│ │
└─────────────────────────────────────────────────────┘
Member 는 회원이죠. 이름, 사용자명 같은 정보를 들고 있고 등급(Grade)도 있어요. Post 는 게시물이고 이제 상태(PostStatus)도 가졌죠. Comment 는 한 번 달면 안 바뀌는 불변 댓글이었고요. 거기에 Shareable·Commentable 같은 역할 인터페이스, 그리고 여러 Enum 까지. 정말 많이 모았어요.
그런데 여기서 한 가지 이상한 점이 있어요. 이 조각들이 지금은 서로 따로 떨어져 있다는 거예요. 책상 위에 부품만 잔뜩 쌓여 있고, 아직 조립은 안 된 상태인 거죠.
조각들을 잇는 선: 연관관계
진짜 인스타그램을 생각해보면, 이 조각들은 절대 따로 놀지 않아요. 게시물에는 그걸 쓴 사람 이 있고, 댓글에는 그걸 단 사람 이 있고, 회원은 다른 회원을 팔로우 하죠. 즉, 객체들끼리 서로 연결되어 있어요.
이렇게 하나의 객체가 다른 객체를 가리키는 관계 를 연관관계(association) 라고 불러요. 어렵게 들리지만 별거 아니에요. "이 게시물의 작성자는 저 회원이다" 처럼 객체가 다른 객체를 손가락으로 가리키는 거예요. 그 그림을 그려볼게요.
┌──────────┐
│ Member │
│ (회원) │
└──────────┘
│ ▲
"쓴다" │ │ "팔로우한다"
│ └──────────┐ (회원이 회원을 가리킴)
▼ │
┌──────────┐ │
│ Post │────────┘
│ (게시물) │
└──────────┘
│ │
"달린다"│ │ "상태를 가진다"
▼ ▼
┌──────────┐ ┌──────────────┐
│ Comment │ │ PostStatus │
│ (댓글) │ │ (Enum) │
└──────────┘ └──────────────┘
이 그림을 말로 읽으면 이래요. 회원이 게시물을 쓰고, 게시물에는 댓글이 달리고, 게시물은 자기 상태(공개/비공개) 를 가지고, 회원은 다른 회원을 팔로우 해요. 화살표 하나하나가 전부 연관관계예요. 오늘 우리가 할 일은 이 화살표들을 실제 자바 코드로 옮기는 거예요.
이름표 vs 실제 사람
그런데 잠깐, 여러분 이런 생각이 들 수 있어요. "게시물에 작성자 정보는 이미 있지 않았나요?" 맞아요. 지금까지 우리 Post 는 작성자를 이름 문자열 로만 들고 있었어요. 그러니까 작성자의 "이름표" 만 붙여둔 셈이죠. 이게 어떤 차이를 만드는지 그림으로 비교해볼게요.
[ 지금까지 — 이름표만 ] [ 오늘부터 — 실제 사람 ]
┌──────────────────┐ ┌──────────────────┐
│ Post │ │ Post │
│ ──────────────── │ │ ──────────────── │
│ authorName │ │ author ──────────┼──┐
│ = "jaehoon" │ │ (Member 참조) │ │
│ (그냥 글자) │ │ │ │
└──────────────────┘ └──────────────────┘ │
▼
"작성자 이름이 뭐지?" ┌──────────────────┐
→ 알 수 있음 │ Member │
│ ──────────────── │
"작성자 팔로워가 몇 명이지?" │ username=jaehoon │
→ 모름! 이름만 있으니까 │ followers=1240 │
│ grade()="보통" │
└──────────────────┘
"이 사람의 모든 정보"에
바로 닿을 수 있음!
왼쪽처럼 이름표만 들고 있으면, "작성자 이름이 뭐야?" 정도는 답할 수 있어요. 하지만 "이 글 쓴 사람 팔로워가 몇 명이지?", "이 사람 등급이 프리미엄인가?" 같은 걸 물으면 답이 안 나와요. 이름 글자 말고는 가진 게 없으니까요.
오른쪽처럼 실제 Member 객체를 가리키게 만들면, 게시물에서 작성자의 모든 정보 로 바로 건너갈 수 있어요. post.getAuthor().getFollowers() 처럼요. 이름표 대신 진짜 사람을 손에 쥔 셈이죠. 이게 연관관계를 객체 참조로 표현하는 가장 큰 이유예요. 오늘 다음 Step에서 바로 이 작업, 이름표를 실제 사람으로 바꾸는 일부터 해봅니다.
🙋 학생 질문 — "튜터님, 조각들을 따로 배웠는데 왜 이제야 합치나요? 처음부터 같이 만들면 안 됐나요?"
아주 좋은 질문이에요! 사실 의도된 순서예요.
집을 짓는다고 생각해보세요. 벽돌 쌓는 법, 기둥 세우는 법, 지붕 얹는 법을 한 번에 동시에 배우면 머리가 터지겠죠? 그래서 벽돌부터 차근차근 익히는 거예요. 우리도 마찬가지였어요. 클래스 하나 만드는 법(Day 8), 부모-자식 관계(Day 10~11), 뼈대만 정하는 추상 클래스(Day 12), 역할을 정의하는 인터페이스(Day 13), 정해진 선택지를 다루는 Enum(Day 15)을 따로 익혔어요.
만약 이걸 처음부터 다 엮어서 보여줬다면, 화살표가 사방으로 뻗은 복잡한 그림 앞에서 "이게 뭐지?" 하고 멈췄을 거예요. 각 부품을 충분히 손에 익힌 지금이 오히려 합치기 딱 좋은 때예요. 부품을 알아야 조립이 눈에 들어오거든요.
그리고 실무에서도 설계는 이렇게 해요. 작은 단위를 먼저 정의하고, 그것들을 연결하는 거죠. 오늘 우리가 하는 게 바로 그 "연결" 작업이에요.
💡 튜터의 결론
따로 배운
Member·Post·Comment·Enum·인터페이스는 사실 하나의 큰 그림의 부품이었어요. 객체가 다른 객체를 필드로 가리키는 걸 연관관계라고 해요. 오늘은 이 부품들을 화살표로 이어 진짜 인스타그램 도메인 모델로 조립해요.
다음 Step에서는 가장 먼저 게시물의 작성자를 "이름 문자열" 에서 "실제 Member 객체" 로 바꿔서, 첫 번째 연관관계를 코드로 연결해볼게요.
Step 2. 작성자를 실제 Member 객체로 연결하자
자, 이제 그림을 코드로 옮길 시간이에요. Step 1에서 그려본 "이름표 vs 실제 사람" 비교, 기억나시죠? 우리 Post 는 지금까지 작성자를 이름 문자열로만 들고 있었어요. 이번엔 게시물이 실제 Member 객체를 가리키게 만들어서, 첫 번째 연관관계를 진짜 코드로 연결해봅니다.
좋은 소식이 하나 있어요. 우리는 기존 코드를 깨뜨리지 않고 새 길을 하나 더 내는 방식으로 갈 거예요. 지난 시간에 쓰던 코드가 여전히 잘 돌아가게 두면서, 거기에 더 똑똑한 통로를 추가하는 거죠.
기존 생성자는 그대로 두고, 새 생성자를 추가해요
먼저 우리 Post 가 어떤 정보를 들고 있는지 다시 볼게요.
// com/instagram/javabasic/domain/post/Post.java
public class Post implements Commentable {
// 게시물을 이루는 정보 — private 으로 숨겨요
private String content; // 게시물 내용
private String authorName; // 작성자 이름
private int likeCount; // 좋아요 수
private PostStatus status; // 게시물 상태(공개/비공개/보관됨) — enum 으로 표현
// 작성자를 Member 객체로 직접 가리켜요(참조). authorName 은 그 이름을 베껴둔 값이에요.
// 두 가지가 함께 있는 이유는 아래 생성자 설명에서 풀어요.
private Member author;
새로 생긴 줄이 맨 아래 private Member author; 한 줄이에요. 기존 authorName(이름 문자열)은 그대로 있고, 그 옆에 author(실제 회원 객체)라는 통로가 하나 더 생긴 거죠. 둘 다 작성자를 가리키지만, 하나는 이름 글자만, 하나는 진짜 그 사람 전체를 가리켜요.
이제 생성자를 봐요. 지난 시간에 쓰던 생성자는 손대지 않고 그대로 남겨뒀어요.
// 매개변수 생성자 — 세 가지 정보를 받아요. 상태는 기본값(공개)으로 둬요.
public Post(String content, String authorName, int likeCount) {
this.content = content;
this.authorName = authorName;
this.likeCount = likeCount;
this.status = PostStatus.PUBLIC;
}
이건 지난 시간 코드예요. 작성자를 문자열로 받죠. 이걸 지운 게 아니라 그대로 뒀어요. 왜냐면 이걸 쓰던 기존 코드(예전에 작성한 시연 코드들)가 멀쩡히 돌아가야 하니까요. 새 기능을 넣는다고 잘 쓰던 걸 부수면 곤란하잖아요.
그 아래에 새 생성자를 하나 더 추가했어요. 이게 오늘의 핵심이에요.
// 작성자를 Member 객체로 직접 받는 생성자 — 도메인 연결의 핵심이에요.
// 객체를 받는 순간, 그 사람의 이름(getUsername())을 authorName 에도 똑같이 베껴둬요.
// 그래서 "객체로 따라가기(author)" 와 "이름만 보기(authorName)" 두 가지가 늘 같은 사람을 가리켜요.
public Post(String content, Member author, int likeCount) {
this.content = content;
this.author = author;
this.authorName = author.getUsername();
this.likeCount = likeCount;
this.status = PostStatus.PUBLIC;
}
이 생성자가 하는 일을 천천히 읽어볼게요. 두 번째 매개변수가 String authorName 이 아니라 Member author 예요. 이름 글자가 아니라 진짜 회원 객체를 통째로 받는 거죠.
그리고 가운데 줄 this.authorName = author.getUsername(); 이 부분이 정말 영리해요. 회원 객체를 받는 그 순간, 그 사람의 이름(getUsername())을 꺼내서 authorName 에도 똑같이 베껴 적어둬요. 그래서 객체로 따라가는 길(author)과 이름만 보는 길(authorName)이 처음부터 같은 사람을 가리키게 되는 거예요. 둘이 어긋날 일이 없죠.
getAuthor() 로 작성자의 모든 정보를 따라가요
이렇게 연결해두면 뭐가 좋아질까요? 게시물에서 작성자 객체를 통째로 꺼낼 수 있어요.
// 작성자 객체를 그대로 돌려줘요 — 이걸로 작성자의 점수·등급까지 따라갈 수 있어요.
public Member getAuthor() {
return author;
}
getAuthor() 가 회원 객체를 그대로 돌려주니까, 게시물에서 작성자에게로 건너가서 그 사람이 가진 모든 걸 물어볼 수 있어요. 예를 들면 이렇게요.
Member jaehoon = new Member("jaehoon", 1240, 42, 5, 200);
Post post = new Post("오늘 카페에서 찍은 사진", jaehoon, 0);
// 게시물 → 작성자 → 작성자의 등급까지 한 번에 따라가요
System.out.println(post.getAuthorName()); // jaehoon (이름만)
System.out.println(post.getAuthor().getFollowers()); // 1240 (작성자 팔로워 수!)
System.out.println(post.getAuthor().grade()); // 작성자의 추천 등급!
post.getAuthor().grade() 를 보세요. 게시물에서 출발해서 점(.)을 타고 작성자에게 건너간 다음, 작성자의 등급 계산까지 줄줄이 따라갔어요. 이름표만 들고 있을 땐 절대 못 하던 일이죠. 이게 바로 객체를 직접 가리키는 연관관계의 힘이에요.
⚠️ 주의 — 이름표는 만든 그 순간에만 똑같아요
새 생성자에서
this.authorName = author.getUsername()으로 이름을 베껴 적는 건 게시물을 만드는 그 순간 딱 한 번이에요. 만약 회원이 나중에 사용자명을 바꾸면,author(실제 객체)는 새 이름을 따라가지만authorName(베껴둔 글자)은 옛 이름 그대로 남아 있을 수 있어요. 하나는 살아 움직이는 참조, 하나는 그때 찍어둔 사진(스냅샷)인 셈이죠. 이 차이는 나중에 데이터를 저장하거나 화면용 데이터를 만들 때 다시 마주칠 주제예요. 지금은 "베껴둔 값과 살아있는 참조는 다를 수 있다" 정도만 머리 한쪽에 담아두면 충분해요.
🙋 학생 질문 — "튜터님, 실제 객체로 가리킬 거면 authorName 은 이제 지워도 되는 거 아닌가요? 왜 둘 다 두나요?"
날카로운 질문이에요! 충분히 그런 생각이 들 수 있어요.
이유는 두 가지예요. 첫째, 앞에서 본 것처럼 지난 시간에 쓰던 생성자 Post(content, authorName, likeCount) 가 여전히 살아 있어요. 그 생성자로 만든 게시물은 author(객체) 없이 authorName(이름)만 가지고 있죠. 만약 authorName 을 통째로 지워버리면, 그 기존 코드가 전부 깨져버려요. 잘 돌아가던 걸 부수는 셈이죠.
둘째, 이름만 빠르게 보고 싶을 때도 많아요. 게시물 목록을 화면에 쭉 뿌릴 때, 작성자의 팔로워 수나 등급까지는 필요 없고 그냥 이름만 보여주면 되는 경우가 흔하거든요. 그럴 때 getAuthorName() 한 번이면 끝이라 편해요.
그러니까 지금 단계에서는 "이름표(authorName)와 실제 사람(author)을 둘 다 들고, 둘이 같은 사람을 가리키게 맞춰둔다" 가 가장 안전하고 편한 선택이에요. 나중에 데이터를 어떻게 저장하고 다룰지 더 배우면, 둘 중 무엇을 남길지 더 똑똑하게 고를 수 있게 돼요.
💡 튜터의 결론
Post에private Member author;필드와Post(content, Member author, likeCount)생성자를 더해서, 작성자를 이름 문자열이 아니라 실제 회원 객체로 가리키게 만들었어요. 객체를 받는 순간 이름도 함께 베껴두니, 이름으로 보든 객체로 따라가든 늘 같은 사람을 가리켜요. 이제post.getAuthor().grade()처럼 게시물에서 작성자의 점수·등급까지 줄줄이 건너갈 수 있어요.
다음 Step에서는 방향을 반대로 돌려서, 이번엔 회원이 자기가 쓴 글들과 팔로우하는 사람들을 직접 안고 있도록 만들어볼게요. 한 사람이 여러 개를 갖는 1:N 관계예요.
Step 3. 회원이 자기 글과 팔로잉을 갖게 하자
Step 2에서는 게시물이 작성자 한 명을 가리켰죠. 이번엔 화살표 방향을 반대로 돌려봐요. 한 회원은 글을 여러 개 쓰고, 여러 사람을 팔로우하잖아요. 그러니까 이번엔 한 명이 여러 개를 갖는 관계예요. 이런 걸 1:N(일대다) 관계라고 불러요. 회원 한 명(1)에 글이 여러 개(N) 매달리는 거죠.
그런데 여기서 작은 고민이 생겨요. "여러 개" 를 어떻게 담죠? 글 하나는 변수 하나에 담으면 되는데, 개수가 정해지지 않은 여러 개는 어디에 담아야 할까요?
여러 개는 배열 + 카운터로 담아요
지금 우리가 가진 도구 중에 "같은 종류를 여러 개 줄 세워 담는" 그릇이 있죠. 바로 배열이에요. Day 5에서 배운 그 배열이요. 여러 개를 담는 더 편한 그릇은 다음 마디에서 만나게 될 텐데, 지금 단계에서는 배열만으로도 충분히 1:N을 표현할 수 있어요.
회원이 가진 새 필드를 볼게요.
// com/instagram/javabasic/domain/member/Member.java
public class Member {
// 묶음 배열의 처음 크기예요. 작성한 글·팔로잉을 이만큼까지 담아요.
private static final int CAPACITY = 16;
// ... (기존 필드: username, followers, posts ...) ...
// 이 사람이 쓴 글들 — 배열에 모으고, 몇 개 찼는지 카운터로 세요(1:N)
private Post[] writtenPosts = new Post[CAPACITY];
private int writtenPostCount = 0;
// 이 사람이 팔로우하는 사람들 — 같은 방식(배열 + 카운터)으로 모아요
private Member[] following = new Member[CAPACITY];
private int followingCount = 0;
writtenPosts 는 이 사람이 쓴 글들을 담는 Post 배열이에요. 그런데 배열만 있으면 "지금 몇 개나 찼지?" 를 알 수가 없어요. 배열은 16칸으로 미리 만들어두지만, 실제로 글이 3개만 들어 있을 수도 있잖아요. 그래서 writtenPostCount 라는 카운터를 옆에 같이 둬요. 글을 하나 담을 때마다 이 숫자를 1씩 올리면, 지금 몇 개가 들어 있는지 항상 정확히 알 수 있죠.
팔로잉도 똑같은 방식이에요. following 은 이 사람이 팔로우하는 회원들을 담는 Member 배열이고, followingCount 가 그 개수를 세요. 맨 위 CAPACITY = 16 은 배열을 처음 만들 때 칸을 몇 개로 잡을지 정한 상수예요. 16칸까지 담을 수 있다는 뜻이죠.
이게 배열에 차곡차곡 채워지는 모습을 그림으로 볼게요.
writtenPosts 배열 (CAPACITY = 16칸)
┌──────┬──────┬──────┬──────┬──────┬─────┐
│ 글0 │ 글1 │ 글2 │ null │ null │ ... │
└──────┴──────┴──────┴──────┴──────┴─────┘
0 1 2 3 4
writtenPostCount = 3 ← 지금 3개 찼다는 뜻
(글을 하나 더 담으면 index 3 에 들어가고 4로 오름)
글을 담을 때마다 다음 빈 칸(index = count)에 넣고, 카운터를 1 올려요. 그래서 카운터는 늘 "다음에 글이 들어갈 칸 번호" 이자 "지금까지 담긴 개수" 가 돼요.
글과 팔로잉을 담고 꺼내는 메서드
이제 이 배열에 글을 담는 메서드를 봐요.
// 이 사람이 쓴 글 하나를 묶음에 더해요. 자리가 차면 더 담지 않고 넘어가요.
public void addWrittenPost(Post p) {
if (writtenPostCount >= writtenPosts.length) {
System.out.println("작성 글 묶음이 가득 찼어요. 더 담지 않아요.");
return;
}
writtenPosts[writtenPostCount] = p;
writtenPostCount++;
}
public int getWrittenPostCount() {
return writtenPostCount;
}
public Post getWrittenPost(int index) {
return writtenPosts[index];
}
addWrittenPost 는 글 하나를 받아서 배열의 다음 빈 칸(writtenPosts[writtenPostCount])에 넣고, 카운터를 1 올려요. 맨 위 if 는 안전장치예요. 만약 16칸이 다 찼는데 또 담으려고 하면, 배열 밖을 건드려서 에러가 나거든요. 그래서 가득 찼으면 안내 메시지만 출력하고 조용히 넘어가요. getWrittenPostCount() 로 몇 개인지 묻고, getWrittenPost(index) 로 특정 글을 꺼내요.
팔로잉도 거의 똑같은 모양이에요.
// 다른 회원을 팔로우해요. 팔로잉 묶음에 더하고, 자리가 차면 넘어가요.
public void follow(Member target) {
if (followingCount >= following.length) {
System.out.println("팔로잉 묶음이 가득 찼어요. 더 담지 않아요.");
return;
}
following[followingCount] = target;
followingCount++;
}
public int getFollowingCount() {
return followingCount;
}
public Member getFollowing(int index) {
return following[index];
}
follow 는 다른 회원을 받아서 팔로잉 배열에 담아요. 글을 담는 방식과 판박이죠. 패턴이 똑같으니까 한 번 이해하면 둘 다 익힌 셈이에요.
isFollowing — Day 10에서 만든 equals 를 다시 써요
마지막 메서드가 재밌어요. "이 사람을 내가 이미 팔로우하고 있나?" 를 알려주는 isFollowing 이에요.
// 이미 그 사람을 팔로우하고 있는지 — 묶음을 처음부터 훑어 equals 로 비교해요.
// Member.equals 는 username 기준이라, 이름이 같으면 같은 사람으로 봐요.
public boolean isFollowing(Member target) {
for (int i = 0; i < followingCount; i++) {
if (following[i].equals(target)) {
return true;
}
}
return false;
}
팔로잉 배열을 처음부터 끝까지(followingCount 만큼) 훑으면서, 찾는 사람이 있는지 하나씩 비교해요. 여기서 비교에 쓰는 following[i].equals(target) 의 equals, 어디서 본 적 있죠? 맞아요, Day 10에서 우리가 직접 만든 그 equals 예요. Member 의 equals 는 사용자명(username)이 같으면 같은 사람으로 보도록 만들어뒀잖아요.
그때 만들어둔 자산을 지금 그대로 꺼내 쓰는 거예요. 만약 equals 를 정의해두지 않았다면, 자바 기본 동작은 메모리 주소를 비교해서 "글자는 같아도 다른 객체면 다른 사람" 으로 봤을 거예요. 그럼 같은 이름인데도 못 찾는 일이 생기죠. 지난 시간에 미리 깔아둔 길 덕분에 오늘 isFollowing 이 자연스럽게 동작하는 거예요.
💡 튜터의 결론
한 회원이 여러 글과 여러 팔로잉을 갖는 1:N 관계는, 아직 배운 도구 안에서 배열 + 카운터로 표현해요. 배열이 그릇이고 카운터가 "지금 몇 개" 를 세죠.
addWrittenPost·follow로 담고,getWrittenPost·getFollowing으로 꺼내요. 특히isFollowing은 Day 10에서 만든equals(username 기준)를 그대로 재활용해요. 미리 만들어둔 부품이 나중에 이렇게 맞물리는 게 설계의 묘미예요.
다음 Step에서는 팔로우를 또 다른 시선으로 봐요. 지금은 "내 팔로잉 목록에 상대를 담는" 방식이었다면, 이번엔 "누가 누구를 팔로우한다" 는 관계 자체를 하나의 객체로 만들어볼게요.
Step 4. 팔로우 "관계" 를 객체로 만들자
Step 3에서 만든 follow() 메서드는 "내 팔로잉 목록에 상대방을 담는" 일이었어요. 회원 입장에서 "내가 따라가는 사람들" 을 모아둔 거죠. 그런데 팔로우를 조금 다른 눈으로 볼 수도 있어요.
"장원영이 카리나를 팔로우한다" 라는 문장을 다시 읽어보세요. 여기엔 사실 하나의 사건, 하나의 연결 이 있어요. 거는 사람과 받는 사람을 잇는 그 관계 자체요. 이번엔 그 관계를 통째로 하나의 객체로 승격시켜봐요.
왜 관계를 객체로 만들까요?
관계를 객체로 만들면 뭐가 좋을까요? 관계에도 정보를 붙일 수 있게 돼요. 예를 들어 "누가" "누구를" 팔로우했는지는 기본이고, 나중에 "언제 팔로우했는지" 같은 정보까지 이 관계 객체에 담을 수 있죠. 관계가 그냥 목록 속 항목이 아니라, 자기 정보를 가진 어엿한 객체가 되는 거예요.
새로 만든 Follow 클래스를 봐요.
// com/instagram/javabasic/domain/follow/Follow.java
public class Follow {
// 팔로우를 거는 사람 (예: 내가 상대를 팔로우)
private final Member follower;
// 팔로우를 받는 사람 (예: 상대)
private final Member followee;
// 두 사람을 받아 관계를 완성해요. 한 번 정해지면 못 바꿔요.
public Follow(Member follower, Member followee) {
this.follower = follower;
this.followee = followee;
}
public Member getFollower() {
return follower;
}
public Member getFollowee() {
return followee;
}
// 관계를 한눈에 보이게 — "@거는사람 → @받는사람" 형식으로 출력해요.
@Override
public String toString() {
return "@" + follower.getUsername() + " → @" + followee.getUsername();
}
}
Follow 객체는 두 Member 를 들고 있어요. follower 는 팔로우를 거는 사람, followee 는 받는 사람이에요. 이름이 비슷해서 헷갈릴 수 있는데, "-er" 로 끝나는 follower 가 따라가는 쪽(거는 사람), "-ee" 로 끝나는 followee 가 따라옴을 당하는 쪽(받는 사람)이라고 기억하면 편해요.
이걸 그림으로 보면 두 회원을 잇는 다리 같아요.
┌──────────┐ ┌──────────┐
│ Member │ ──── Follow 객체 ──→ │ Member │
│ jaehoon │ (관계 한 건) │ minji │
└──────────┘ └──────────┘
follower followee
(거는 사람) (받는 사람)
Follow 객체 하나가 두 사람을 이어주는 "연결선" 역할을 해요.
Follow 객체 하나가 곧 화살표 하나, 즉 관계 한 건이에요. Step 1에서 그렸던 "회원이 회원을 가리킨다" 는 화살표를 이제 진짜 객체로 만든 거죠.
final 로 잠가서 못 바꾸게 했어요
두 필드를 다시 보면 앞에 final 이 붙어 있어요.
private final Member follower;
private final Member followee;
final 은 "한 번 값을 정하면 다시는 못 바꾼다" 는 잠금장치예요. Day 14에서 불변(immutable)을 배웠고, 댓글(Comment)을 한 번 달면 안 바뀌게 만들었던 거 기억나시죠? 같은 생각이에요. 팔로우 관계도 한 번 "jaehoon이 minji를 팔로우했다" 로 정해지면, 도중에 "사실은 다른 사람이었어" 하고 슬쩍 바꿀 일이 없어요. 그러니 아예 못 바꾸게 잠가두면 더 안전하죠. 생성자에서 두 사람을 받아 채운 뒤로는, getter 로 읽기만 할 뿐 바꾸는 통로(setter)가 아예 없어요.
써보면 이렇게 동작해요.
Member jaehoon = new Member("jaehoon", 1240, 42, 5, 200);
Member minji = new Member("minji", 8500, 150, 3, 365);
Follow follow = new Follow(jaehoon, minji);
System.out.println(follow); // @jaehoon → @minji
toString 을 새로 정의해둔 덕분에, 관계 객체를 그대로 출력하면 알아보기 힘든 주소 대신 @jaehoon → @minji 처럼 화살표로 깔끔하게 보여줘요.
🙋 학생 질문 — "튜터님, Step 3에서 follow() 로 팔로우 다 했는데 왜 Follow 객체를 또 만드나요? 중복 아닌가요?"
좋은 의문이에요! 둘은 비슷해 보이지만 바라보는 시선이 달라요.
Step 3의 follow() 는 회원 입장이에요. "내가 따라가는 사람들" 을 내 목록 안에 모아두는 거죠. 회원 한 명을 중심으로 생각하는 방식이에요.
반면 Follow 객체는 관계 그 자체를 중심에 둬요. "jaehoon → minji" 라는 연결 한 건을 독립된 물건으로 본 거죠. 이렇게 관계를 객체로 떼어내면, 나중에 그 관계에 정보를 붙이기 좋아요. "언제 팔로우했는지", "맞팔인지" 같은 걸 관계 객체에 담을 수 있거든요. 회원 목록 속 항목으로는 이런 추가 정보를 붙이기가 어려워요.
지금 단계에서는 두 방식을 둘 다 보여드리는 게 목적이에요. 같은 팔로우라도 "목록에 담기" 와 "관계를 객체로 만들기" 라는 두 시선이 있다는 걸 알아두면, 나중에 더 큰 설계를 할 때 어느 쪽이 맞는지 고를 수 있게 돼요.
💡 튜터의 결론
Follow클래스는 "누가(follower) 누구를(followee) 팔로우한다" 는 관계 자체를 하나의 객체로 만든 거예요. 두Member를 가리키는 객체죠. 한 번 맺어진 관계는 바뀌지 않으니 두 필드를final로 잠가 불변으로 만들었어요(Day 14의 불변,Comment와 같은 생각). 관계를 객체로 떼어두면 나중에 관계에 정보를 붙이기 좋아요.
여기까지 화살표들을 하나씩 코드로 옮겨봤어요. 게시물이 작성자를 가리키고, 회원이 글과 팔로잉을 안고, 팔로우 관계가 객체가 됐죠. 다음 Step에서는 이렇게 만든 도메인을 더 깔끔하게 정리해봐요. 여기저기 흩어진 공통 행동을 인터페이스로 묶어서, 모델을 한층 단정하게 다듬어볼게요.
Step 5. 인터페이스로 공통 행동 묶기 — Post가 "댓글 달 수 있음"을 약속하다
자, 이제 좀 흥미로운 이야기를 해볼게요. Step 2에서 Post 클래스 첫 줄을 봤을 때 이런 게 적혀 있었던 거 기억하세요?
public class Post implements Commentable {
그때는 "일단 이렇게 적혀 있구나" 하고 넘어갔는데, 이번 Step에서 이 implements Commentable 이 진짜 무슨 뜻인지 제대로 풀어볼게요. 지난 시간(Day 13)에 배운 인터페이스가 여기서 다시 등장합니다.
인터페이스는 "이런 행동을 할 수 있다"는 약속이에요
Day 13에서 인터페이스를 "리모컨 버튼" 에 비유했던 거 떠올려보세요. 인터페이스는 "이런 기능을 제공하겠다" 는 약속, 즉 계약이에요. 안에 들어 있는 메서드는 본문({} 안의 내용) 없이 이름만 적혀 있어요. "이 일을 하겠다고 약속만 하고, 실제로 어떻게 할지는 그 약속을 받아든 클래스가 채운다" 는 식이죠.
우리 도메인에는 Commentable 이라는 인터페이스가 있어요. 이름 그대로 "댓글을 달 수 있다(Comment + able)" 는 역할이에요. 한번 펼쳐볼게요.
// com/instagram/javabasic/domain/content/Commentable.java
public interface Commentable {
// 인터페이스 안의 변수는 자동으로 public static final 이에요(상수).
// 한 콘텐츠에 달 수 있는 댓글의 최대 개수예요.
int MAX_COMMENTS = 100;
// 약속 — 댓글을 단다 / 현재 댓글 수를 돌려준다. 본문은 구현 클래스가 채워요.
void addComment(String text);
int getCommentCount();
// static 메서드 — 객체 없이 인터페이스 이름으로 바로 부를 수 있어요(Commentable.isFull(...)).
// 지금 댓글 수가 한도에 찼는지 검사하는 공통 도우미예요.
static boolean isFull(int currentCount) {
return currentCount >= MAX_COMMENTS;
}
}
이 인터페이스 안에 세 종류의 것이 들어 있어요. 하나씩 볼게요.
먼저 int MAX_COMMENTS = 100; 이에요. 인터페이스 안에 적은 변수는 그냥 변수가 아니라 자동으로 상수(public static final)가 돼요. 따로 final 을 안 붙여도 자바가 알아서 "절대 안 바뀌는 공용 값" 으로 만들어주는 거예요. 그래서 한 게시물에 댓글을 최대 100개까지 달 수 있다는 규칙이 여기 한 곳에 적혀 있게 됩니다.
다음은 void addComment(String text); 와 int getCommentCount(); 예요. 둘 다 본문 없이 이름과 괄호만 있고 세미콜론으로 끝나죠? 이게 바로 약속이에요. "댓글을 다는 기능과 댓글 수를 알려주는 기능을 제공하겠다" 고 선언만 한 거예요. 실제 동작은 이 인터페이스를 구현하는 클래스가 채워요.
마지막은 static boolean isFull(int currentCount) 예요. 이건 static 메서드라 본문이 채워져 있어요. 객체를 따로 만들지 않고 Commentable.isFull(...) 처럼 인터페이스 이름으로 바로 부를 수 있는 공통 도우미예요. "지금 댓글 수가 한도에 찼니?" 를 한 줄로 판단해주죠.
Post가 이 약속을 채워요
이제 Post 가 implements Commentable 이라고 선언했다는 건, "나는 댓글 다는 기능을 제공하겠다고 약속한다" 는 뜻이에요. 약속했으면 채워야겠죠? Post 안에서 그 약속을 어떻게 채우는지 보겠습니다.
// com/instagram/javabasic/domain/post/Post.java
// 이 게시물에 달린 댓글들 — 배열 하나에 모아요(1:N). 몇 개 찼는지는 commentCount 로 세요.
private Comment[] comments = new Comment[Commentable.MAX_COMMENTS];
private int commentCount = 0;
// 댓글 객체를 직접 받아 배열에 담아요. 한도(MAX_COMMENTS)에 차면 더 담지 않고 넘어가요.
public void addComment(Comment c) {
if (Commentable.isFull(commentCount)) {
System.out.println("댓글이 한도(" + Commentable.MAX_COMMENTS + "개)에 찼어요. 더 담지 않아요.");
return;
}
comments[commentCount] = c;
commentCount++;
}
먼저 위쪽을 보면, 댓글을 담을 배열의 크기를 Commentable.MAX_COMMENTS 로 정했어요. 인터페이스에 적어둔 상수 100을 그대로 가져다 쓴 거예요. 게시물이 여러 댓글을 갖는 1:N 관계는 Step 3에서 본 "배열 + 카운터" 그대로예요. comments 배열이 그릇이고, commentCount 가 지금 몇 개 담겼는지 세요.
addComment(Comment c) 는 댓글 객체를 직접 받아 배열에 담아요. 담기 전에 Commentable.isFull(commentCount) 로 "지금 한도에 찼나?" 를 먼저 물어봐요. 찼으면 안내 문구만 출력하고 그냥 돌아가죠(return). 안 찼으면 배열의 다음 칸에 댓글을 넣고 카운터를 하나 올려요. 한도 검사 규칙(100개)이 인터페이스 한 곳에 모여 있으니, 여기서는 그냥 불러 쓰기만 하면 돼서 깔끔하죠.
그런데 잠깐, 인터페이스가 약속한 건 addComment(Comment c) 가 아니라 addComment(String text) 였잖아요? 이 약속은 어떻게 채울까요?
// com/instagram/javabasic/domain/post/Post.java
// Commentable 약속 — 텍스트만 받으면 작성자 이름으로 댓글 객체를 만들어 담아요.
// commenter 정보가 따로 없으니, 글 작성자 이름을 쓰고(없으면 "익명") 좋아요 0 으로 시작해요.
@Override
public void addComment(String text) {
String commenter = (authorName != null) ? authorName : "익명";
addComment(new Comment(commenter, text, 0));
}
// Commentable 약속 — 지금까지 달린 댓글 수를 돌려줘요.
@Override
public int getCommentCount() {
return commentCount;
}
// 인덱스로 댓글 하나를 꺼내요.
public Comment getComment(int index) {
return comments[index];
}
@Override 가 붙은 addComment(String text) 가 바로 인터페이스 약속을 채우는 메서드예요. @Override 는 Day 10에서 배운, "이건 위에서 약속한 걸 채우는 거야" 라고 표시해두는 꼬리표죠. 텍스트만 받으면 작성자가 누군지 정보가 따로 없으니, 글 작성자 이름(authorName)을 쓰고, 그마저 없으면 "익명" 으로 두고 좋아요 0인 Comment 객체를 만들어요. 그리고 바로 위에서 본 addComment(Comment c) 를 다시 불러서 담죠.
여기서 재밌는 게 보이시나요? addComment 라는 같은 이름의 메서드가 두 개 있어요. 하나는 Comment 객체를 받고, 하나는 String 텍스트를 받죠. 이게 Day 6에서 배운 오버로딩이에요. 이름은 같지만 받는 게 달라서 자바가 알아서 구분해줘요. 텍스트만 받는 쪽이 객체를 만들어서 객체 받는 쪽한테 일을 넘기는, 둘이 손발을 맞추는 구조예요.
getCommentCount 도 @Override 가 붙어서 인터페이스 약속을 채우고, 지금까지 담긴 댓글 수를 돌려줘요. getComment(int index) 는 인터페이스 약속은 아니지만, 배열에서 댓글 하나를 꺼내 보여주는 편의 메서드예요.
약속과 구현을 그림으로 보면
지금 일어난 일을 한 장으로 정리해볼게요.
┌─────────────────────────────┐
│ Commentable (인터페이스) │ ← "댓글 달 수 있음" 이라는 약속
│ ─────────────────────── │
│ MAX_COMMENTS = 100 (상수) │
│ addComment(String) (약속) │
│ getCommentCount() (약속) │
│ isFull(int) (도우미) │
└─────────────┬───────────────┘
│ implements (약속을 받아 채움)
▼
┌─────────────────────────────┐
│ Post (클래스) │ ← 약속을 실제로 채운 쪽
│ ─────────────────────── │
│ addComment(String) 채움 ✅ │
│ getCommentCount() 채움 ✅ │
│ + addComment(Comment) 추가 │
└─────────────────────────────┘
위가 약속, 아래가 그 약속을 채운 쪽이에요. Post 가 Commentable 을 구현했다는 건 "이 게시물은 댓글 기능을 분명히 제공한다" 고 보장한 거예요. 누군가 Post 를 받아서 Commentable 의 역할만 보고 싶으면, 댓글 관련 기능이 반드시 있다고 믿고 쓸 수 있죠.
🙋 학생 질문 — "튜터님, MAX_COMMENTS 같은 상수를 그냥 Post 안에 적으면 안 되나요? 왜 인터페이스에 두나요?"
아주 좋은 질문이에요! Post 안에만 적어도 당장은 잘 돌아가요. 그런데 인터페이스에 두면 좋은 점이 있어요.
지금은 Post 만 댓글을 달 수 있지만, 나중에 다른 종류의 콘텐츠도 댓글을 달 수 있게 만들고 싶다고 해봐요. 그때 각각의 클래스마다 "댓글 최대 100개" 를 따로따로 적으면, 나중에 "150개로 늘리자" 가 됐을 때 여러 군데를 다 고쳐야 해요. 한 곳을 깜빡하면 어떤 콘텐츠는 100개, 어떤 건 150개가 되는 버그가 생기죠.
반대로 공통 규칙을 Commentable 한 곳에 모아두면, MAX_COMMENTS 든 isFull 이든 거기만 고치면 댓글을 다는 모든 콘텐츠가 한꺼번에 같은 규칙을 따르게 돼요. "공통 규칙과 공통 도우미는 약속한 곳에 모아둔다" 가 인터페이스를 쓰는 큰 이유 중 하나예요.
💡 튜터의 결론
인터페이스는 "이런 행동을 할 수 있다" 는 약속(계약)이에요.
Post implements Commentable은 "이 게시물은 댓글 기능을 보장한다" 는 선언이고,@Override가 붙은addComment(String)·getCommentCount()가 그 약속을 실제로 채워요.MAX_COMMENTS(상수)·isFull(도우미) 같은 공통 규칙은 인터페이스 한 곳에 모아두면, 댓글 기능을 가진 모든 콘텐츠가 같은 규칙을 따르게 돼서 관리가 편해져요.
여기까지 흩어져 있던 공통 행동을 인터페이스로 묶어봤어요. 이제 우리 도메인의 조각들이 다 준비됐어요. 다음 Step에서는 이 조각들을 한자리에 모아서, 진짜로 한번 돌려봐요. 회원이 글을 쓰고, 댓글이 달리고, 팔로우가 이어지는 모습을 직접 실행해서 눈으로 확인할 거예요.
Step 6. 도메인이 살아 움직이는 순간 — 통합 시연
자, 이제 진짜 재밌는 순간이에요. 지금까지 우리는 부품을 하나씩 따로 만들었어요. 회원도 만들고, 게시물도 만들고, 댓글도 만들고, 팔로우 관계도 만들었죠. 그런데 부품을 따로 보면 "이게 진짜 인스타처럼 돌아가긴 하는 걸까?" 싶잖아요. 이번 Step에서 그 조각들을 한자리에 모아서 실제로 돌려봐요. 회원이 글을 쓰고, 그 글에 댓글이 달리고, 회원끼리 팔로우하는 — 인스타의 뼈대가 움직이는 모습을 직접 볼 거예요.
시연 코드를 통째로 읽어봐요
아래는 우리 도메인 조각들을 한자리에서 엮는 시연 프로그램이에요. 좀 길지만, 천천히 위에서부터 따라 읽으면 흐름이 보여요.
// com/instagram/javabasic/domain/DomainModelDemo.java
public class DomainModelDemo {
public static void main(String[] args) {
// 1) 회원 세 명을 만들어요
Member jaehoon = new Member("jaehoon_dev", 1240, 42, 8, 120);
Member minji = new Member("minji_cafe", 8500, 150, 23, 365);
Member seungwoo = new Member("seungwoo", 320, 12, 2, 30);
// 2) jaehoon 이 글을 써요 — 작성자를 이름이 아니라 "객체" 로 직접 넘겨요
Post post = new Post("오늘 점심 맛집 추천!", jaehoon, 12);
jaehoon.addWrittenPost(post);
// 글에서 작성자를 따라가면 그 사람의 점수·등급까지 바로 알 수 있어요
Member writer = post.getAuthor();
System.out.println("글 작성자: @" + writer.getUsername()
+ " (점수 " + writer.calculateRecommendScore() + ", 등급 " + writer.grade() + ")");
System.out.println("이름만 베껴둔 값(authorName): " + post.getAuthorName());
// 3) 글에 댓글을 달아요 — 댓글 객체로도, 텍스트만으로도 달 수 있어요
post.addComment(new Comment("minji_cafe", "어디예요? 저도 갈래요!", 0));
post.addComment("좋아요 누르고 갑니다");
System.out.println("댓글 수: " + post.getCommentCount());
for (int i = 0; i < post.getCommentCount(); i++) {
System.out.println(" - " + post.getComment(i));
}
// 4) jaehoon 이 minji 와 seungwoo 를 팔로우해요
jaehoon.follow(minji);
jaehoon.follow(seungwoo);
System.out.println("@jaehoon_dev 팔로잉 수: " + jaehoon.getFollowingCount());
System.out.println("minji 를 팔로우 중인가? " + jaehoon.isFollowing(minji));
// 5) 관계 자체를 객체로 만들어 출력해요
Follow follow = new Follow(jaehoon, minji);
System.out.println("관계 객체: " + follow);
}
}
흐름을 한 단계씩 따라가봐요
번호대로 한 단계씩 짚어볼게요.
1번에서 회원 세 명(jaehoon, minji, seungwoo)을 만들어요. 각자 사용자명·팔로워 수·게시물 수 같은 정보를 들고 태어나죠. Day 8에서 만든 Member 생성자 그대로예요.
2번이 오늘의 핵심이에요. jaehoon 이 글을 쓰는데, new Post("오늘 점심 맛집 추천!", jaehoon, 12) 처럼 작성자 자리에 이름 문자열이 아니라 jaehoon 객체를 통째로 넘겨요. Step 2에서 만든 "작성자를 Member 객체로 받는 생성자" 가 여기서 동작하는 거예요. 그리고 jaehoon.addWrittenPost(post) 로 회원이 자기가 쓴 글 목록에도 이 글을 담죠(Step 3의 1:N).
이게 왜 강력한지 바로 다음 줄에서 드러나요. post.getAuthor() 로 글의 작성자를 따라가면 Member 객체가 나오고, 그 객체에 .calculateRecommendScore(), .grade() 를 이어서 부를 수 있어요. 즉 글 하나만 손에 들고 있어도 "이 글 쓴 사람의 추천 점수는 몇 점이고 등급은 뭐지?" 까지 줄줄이 따라갈 수 있는 거예요. 객체가 객체를 직접 가리키니까 가능한 일이죠. 이름 문자열만 들고 다니던 옛날 방식이었다면, 점수를 알려면 그 이름으로 회원을 다시 찾아 헤매야 했을 거예요.
3번에서 글에 댓글을 달아요. 첫 줄은 Comment 객체를 직접 만들어서 달고, 둘째 줄은 "좋아요 누르고 갑니다" 처럼 텍스트만 넘겨서 달아요. Step 5에서 본 두 가지 addComment 가 둘 다 쓰이는 거예요. 그 다음 getCommentCount() 로 댓글이 몇 개 달렸는지 세고, for 반복문으로 하나씩 꺼내 출력하죠.
4번에서 jaehoon 이 minji 와 seungwoo 를 팔로우해요. getFollowingCount() 로 팔로잉 수를 세고, isFollowing(minji) 로 "내가 minji 를 팔로우 중인가?" 를 물어봐요. 이 isFollowing 이 Step 3에서 봤듯이 Day 10의 equals 를 그대로 재활용하는 메서드죠.
5번에서 jaehoon → minji 라는 관계 한 건을 Follow 객체로 만들어 출력해요. Step 4에서 toString 을 정의해둔 덕분에 깔끔한 화살표로 보이고요.
실행하면 이런 결과가 나와요
IntelliJ에서 이 main 을 실행하면(Run 버튼), 콘솔에 대략 이런 내용이 흘러나와요.
- 글 작성자가
@jaehoon_dev이고, 추천 점수와 등급까지 같이 찍혀요. 글 하나에서 작성자 객체를 따라가 점수·등급을 읽어온 결과예요. - 이름만 베껴둔 값(
authorName)도jaehoon_dev로 똑같이 나와요. 객체로 따라간 길과 이름만 본 길이 같은 사람을 가리킨다는 확인이죠. - 댓글 수는
2로 찍히고, 댓글 두 개가@작성자: 내용 (좋아요 N)형식으로 한 줄씩 보여요. - 팔로잉 수는
2, 그리고 "minji 를 팔로우 중인가?" 에는true가 나와요. - 마지막 관계 객체는
@jaehoon_dev → @minji_cafe라는 화살표로 깔끔하게 출력돼요.
이게 바로 인스타그램의 뼈대가 실제로 돌아가는 모습이에요. 회원이 글을 쓰고, 글이 작성자를 알고, 글에 댓글이 쌓이고, 회원끼리 팔로우로 이어지는 — 우리가 매일 쓰는 인스타의 핵심 흐름이 순수 Java 객체들의 연결로 살아 움직이는 거죠. 화면도 없고 데이터베이스도 없지만, "두뇌" 에 해당하는 도메인 모델은 이미 다 갖춰진 셈이에요.
💡 튜터의 결론
통합 시연은 따로 만든 조각들(
Member·Post·Comment·Follow)이 실제로 맞물려 도는지 확인하는 단계예요. 핵심은 "객체가 객체를 직접 가리킨다" 는 점이에요. 글 하나에서getAuthor()로 작성자 객체를 따라가 그 사람의 점수·등급까지 줄줄이 읽어낼 수 있죠. 이름 문자열만 들고 다니던 방식과 비교하면, 객체 참조가 얼마나 편한지 이 시연 하나로 확 와닿아요.
여기까지 우리가 만든 도메인이 실제로 돌아가는 걸 눈으로 확인했어요. 이제 마지막으로, 그동안 우리가 걸어온 Phase 2 전체를 돌아보며 정리하는 시간을 가져볼게요.
Step 7. Phase 2 회고 — 언어의 산을 넘은 우리
수고 많으셨어요! 오늘은 새 문법을 배우는 대신, 그동안 모은 무기들을 한자리에 모아 진짜 도메인 모델로 엮어봤죠. 이번 Step은 코드를 짜는 시간이 아니라, Day 8부터 여기까지 걸어온 길을 한번 쭉 돌아보는 회고 시간이에요.
Day 8부터 16까지, 우리가 넘어온 계단
객체지향이라는 큰 산을 한 계단씩 올라왔어요. 한 줄씩 압축해서 다시 떠올려볼게요.
Day 16 통합 ──────────────────────────── 🏔️ 정상
(조각들을 도메인 모델로 엮다) │
Day 15 Enum ──────────────────────── │
(정해진 값만 — PostStatus) │
Day 13 인터페이스 ────────────── │
(할 수 있다는 약속 — Commentable) │
Day 12 추상 클래스 ───────── │
(뼈대만 그린 부모) │
Day 11 다형성 ───────── │
(같은 명령, 다른 동작) │
Day 10 상속 ────── │
(부모 것을 물려받기) │
Day 9 캡슐화 ─── │
(숨기고 통로만 열기) │
Day 8 클래스/객체 ─ │
(붕어빵 틀과 붕어빵) ← 출발점 ┘
Day 8에서 클래스라는 "붕어빵 틀" 과 객체라는 "붕어빵" 으로 시작했어요. Day 9에서는 필드를 private 으로 숨기고 통로(getter/setter)만 여는 캡슐화를 배웠죠. Day 10에서 부모 것을 물려받는 상속, Day 11에서 같은 명령에 객체마다 다르게 반응하는 다형성, Day 12에서 뼈대만 그려두는 추상 클래스, Day 13에서 "할 수 있다는 약속" 인 인터페이스, Day 15에서 정해진 값만 허용하는 Enum까지 왔어요. 그리고 오늘 Day 16에서 이 모든 걸 한데 모아 인스타그램 도메인 모델로 엮어냈고요.
따로 배울 때는 "이걸 대체 어디에 쓰나" 싶었던 개념들이, 오늘 한자리에 모이니 전부 제 역할을 하더라고요. 상속·다형성·인터페이스가 다 우리 도메인 안에서 실제로 동작했죠.
오늘 완성한 도메인 모델을 한눈에
오늘 우리가 엮어낸 인스타그램의 "두뇌" 를 한 장으로 정리하면 이래요.
┌──────────────┐
글 작성 │ Member │ 팔로우(1:N)
┌────────→ │ (회원) │ ────────────┐
│ └──────────────┘ ▼
┌──────────┐ author(참조) ┌──────────────────┐
│ Post │ ───────────→ Member │ following[] 배열 │
│ (게시물) │ │ (1:N + isFollowing)│
└────┬─────┘ └──────────────────┘
│ comments[] (1:N) │
│ implements Commentable 관계를 객체로
▼ ▼
┌──────────┐ 상태(enum) ┌──────────┐
│ Comment │ PostStatus │ Follow │
│ (댓글) │ (공개/비공개/보관됨) │ (관계 객체)│
└──────────┘ └──────────┘
Member(회원)가 중심에서 글도 쓰고 팔로우도 해요.Post(게시물)는 작성자를Member객체로 직접 가리키고(참조), 댓글을 배열로 모아요(1:N).Comment(댓글)는 한 번 만들면 안 바뀌는 불변 객체예요(Day 14).PostStatus(Enum)가 게시물의 공개 상태를 정해진 값으로만 표현하고, 공유 가능 여부 규칙까지 품고 있어요(Day 15).Commentable(인터페이스)이 "댓글 달 수 있음" 이라는 공통 역할을 약속하고,Post가 그걸 채워요(Day 13).Follow(관계 객체)는 "누가 누구를 팔로우" 라는 연결 한 건을 독립된 객체로 만들었어요.
화면도 없고 저장소도 없지만, 인스타가 동작하는 데 필요한 핵심 개념들이 객체와 객체의 연결로 이미 다 표현돼 있어요. 이게 도메인 모델의 힘이에요.
마무리
- Step 1: 따로 만든 클래스들이 사실은 하나의 큰 관계도였음을 한 장으로 펼쳐봤어요.
- Step 2: 작성자를 이름 문자열이 아니라 실제
Member객체로 연결했어요(연관관계, 참조). - Step 3: 회원이 자기 글 목록과 팔로잉 목록을 갖는 1:N 관계를 배열 + 카운터로 표현했어요.
- Step 4: 팔로우 관계를
Follow객체로 떼어내 "관계 그 자체" 를 표현했어요. - Step 5: 공통 행동을
Commentable인터페이스로 묶고,Post가 그 약속을 채웠어요. - Step 6: 조각들을 한자리에 모아 실제로 돌려보며, 인스타의 뼈대가 살아 움직이는 걸 확인했어요.
- Step 7: Day 8~16의 객체지향 여정을 회고하며 도메인 모델 전체를 정리했어요.
다음 시간엔 — 문자열(String)을 제대로 다뤄봐요
오늘 코드를 짜면서 슬쩍 지나간 게 하나 있어요. Step 3에서 isFollowing 을 만들 때, 두 회원이 같은 사람인지 비교하면서 equals 를 썼던 거 기억나세요? Step 2에서 작성자 이름을 비교할 때도 마찬가지였고요. 그때 "왜 == 가 아니라 equals 를 쓰지?" 하는 의문이 살짝 스쳤다면, 다음 시간(Day 17)이 바로 그 답을 푸는 자리예요.
다음 시간엔 우리가 매번 써왔지만 제대로 들여다본 적 없는 문자열(String)을 본격적으로 파봐요. 구체적으로는 이런 것들을 정확하게 짚을 거예요.
- 문자열을 비교할 때 왜
==가 아니라equals를 써야 하는지 — 오늘 무심코 쓴 그equals의 진짜 이유예요. String이 왜 한 번 만들면 안 바뀌는지(불변) — Day 14에서 만난 불변이 사실String에도 숨어 있었어요.- 문자열을 많이 이어붙일 때 더 좋은 도구는 뭔지 —
+로 계속 붙이는 게 왜 비효율적일 수 있는지까지요.
매일 쓰면서도 제대로 몰랐던 문자열의 속을 들여다보면, 그동안 "그냥 되니까 썼던" 코드들이 "아, 그래서 이렇게 동작했구나" 로 바뀔 거예요.
오늘로 Phase 2가 끝났어요. 클래스로 시작해서 도메인 모델 완성까지, 객체지향이라는 산을 통째로 넘어오신 거예요. 처음엔 붕어빵 틀 하나도 어색했을 텐데, 이제는 객체들이 서로를 가리키며 맞물려 도는 구조를 직접 설계하고 실행까지 하셨잖아요. 정말 큰 걸음을 떼신 거예요. 다음 마디(Phase 3)부터는 자바가 기본으로 제공하는 강력한 도구들 — 문자열 처리부터 컬렉션, 예외 처리까지 — 을 하나씩 손에 익히면서, 우리 도메인 모델을 더 단단하게 다듬어갈 거예요. 잠깐 숨 고르고, 다음 시간에 또 만나요!
과제
오늘 배운 연관관계는 머리로 이해하는 것보다 직접 객체끼리 연결해봐야 진짜 내 것이 돼요. 세 가지 과제를 준비했어요. 모두 오늘까지 배운 문법(클래스·상속·인터페이스·final·배열·for·if·equals·@Override)만으로 풀 수 있어요. 컬렉션(List·Map)이나 람다 같은 건 아직 안 배웠으니, 1:N 은 배열 + 카운터로만 표현해주세요.
[기초] 과제 1 — 댓글에 작성자를 Member 객체로 연결하기
해야 할 일
Comment(댓글)가 작성자를 이름 문자열이 아니라 실제 Member 객체로 가리키게 만들어보세요.
상황
오늘 Post(게시물)에서는 작성자를 String authorName(이름)뿐 아니라 Member author(실제 객체)로도 가리키게 만들었죠. 그래서 글에서 작성자의 등급·팔로워 수까지 줄줄이 따라갈 수 있었고요. 그런데 Comment(댓글)는 아직 작성자를 이름 문자열로만 들고 있어요. 댓글에서도 "이 댓글 쓴 사람" 의 등급이나 팔로워 수를 보고 싶을 때가 있겠죠. 그래서 댓글에도 똑같이 작성자 객체를 달아줄 차례예요.
요구사항
Comment에private Member commenter;필드를 하나 더 추가하세요(기존 이름 문자열 필드는 그대로 두세요).- 작성자를
Member객체로 받는 새 생성자를 추가하세요. Step 2의Post(content, Member author, likeCount)패턴 그대로예요. 객체를 받는 순간 그 사람의 이름(getUsername())을 기존 이름 필드에도 똑같이 베껴두세요. getCommenter()로 작성자 객체를 그대로 돌려주는 메서드를 만드세요.main에서 회원 하나로 댓글을 만든 뒤,comment.getCommenter().grade()처럼 댓글에서 작성자의 등급까지 따라가 출력해보세요.
힌트
- Step 2의
Post생성자(this.authorName = author.getUsername();)가 정확히 이 패턴이에요. 그대로Comment에 옮겨오면 돼요. - 기존 생성자를 지우지 말고 새 생성자를 하나 더 두면, 이름만 받던 옛 방식과 객체를 받는 새 방식이 둘 다 살아 있어요(Day 6 오버로딩).
[응용] 과제 2 — 회원에게 "나를 팔로우하는 사람들"(팔로워) 배열 달기
해야 할 일
Member 에 팔로잉(내가 팔로우하는 사람들) 배열과 짝을 이루는, "나를 팔로우하는 사람들"(팔로워) 배열을 추가해보세요.
상황
오늘 Member 에 following(내가 팔로우하는 사람들) 배열을 만들었죠. 그런데 인스타에는 반대 방향도 있어요. 바로 "나를 팔로우하는 사람들" — 팔로워예요. 내 팔로워가 몇 명인지, 특정 사람이 나를 팔로우하는지 알고 싶을 때가 많잖아요. 오늘 만든 following 배열과 정확히 똑같은 패턴을, 방향만 반대로 해서 하나 더 만들어보는 거예요.
요구사항
Member에private Member[] followers = new Member[CAPACITY];와private int followerCount = 0;를 추가하세요(following과 짝을 이루는 형태예요).addFollower(Member m)로 팔로워를 배열에 담는 메서드를 만드세요. Step 3의follow(또는addFollowing)와 똑같이, 배열이 가득 찼는지 먼저 검사하고 안 찼을 때만 담으세요.getFollowerCount()로 팔로워 수를,getFollower(int index)로 배열에서 한 명씩 꺼내는 메서드를 만드세요.main에서 회원 하나에 팔로워 두세 명을addFollower로 담고, 팔로워 수와 각 팔로워 이름을for반복문으로 출력해보세요.
힌트
- Step 3의
following배열 +followingCount+follow+getFollowingCount+getFollowing묶음이 정답 형태예요. 이름을follower/followee방향으로만 바꾸면 돼요. - 배열이 가득 찼는지 검사하는
if (followerCount >= followers.length)줄을 잊지 마세요. 빠뜨리면 칸을 넘어가 에러가 나요.
[심화] 과제 3 — 차단(Block) 관계를 객체로 만들기
해야 할 일
오늘 만든 Follow 패턴을 본떠서, "누가 누구를 차단했다" 는 차단 관계를 독립된 객체로 만들어보세요.
상황
인스타에는 팔로우 말고도 "차단(Block)" 이라는 관계가 있어요. "내가 어떤 사람을 차단했다" 는 것도 결국 두 회원을 잇는 연결 한 건이죠. 오늘 우리는 팔로우라는 연결을 Follow 객체로 떼어내, "관계 그 자체" 를 다뤘어요. 차단도 똑같이 객체로 만들면, 나중에 "언제 차단했는지", "왜 차단했는지" 같은 정보를 그 객체에 자연스럽게 붙일 수 있어요. 관계에 정보를 붙이고 싶을 때 객체로 승격하는 설계 감각을 길러보는 과제예요.
요구사항
Block이라는 클래스를 만드세요.private final Member blocker;(차단하는 사람)와private final Member blocked;(차단당하는 사람) 두 필드를 두세요.- 두 회원을 받아 채우는 생성자를 만들고, 두 필드 모두
final로 잠가 한 번 정하면 못 바꾸게 하세요. getBlocker()·getBlocked()로 두 사람을 꺼내는 메서드를 만드세요.toString()을@Override해서"@차단한사람 ⊘ @차단당한사람"처럼 관계를 한 줄로 보여주세요.main에서 회원 둘로Block객체를 만들고,System.out.println(block)으로 관계가 한 줄로 찍히는지 확인하세요.
힌트
- Step 4의
Follow클래스가 정확히 이 형태예요(follower/followee→blocker/blocked로 이름만 바꾸면 돼요). toString()안에서 회원 이름은blocker.getUsername()처럼 객체를 따라가 꺼내면 돼요.Follow의toString()을 다시 보면 그대로 떠올라요.
생각해볼 주제
정답이 하나로 정해진 문제가 아니에요. 오늘 도메인을 엮으면서 슬쩍 지나간 설계 갈림길들을, 혼자 곱씹어보거나 동료와 이야기 나눠보면 객체를 보는 눈이 한 뼘 더 자라요.
주제 1 — 양방향으로 서로 가리킬까, 한 방향만 가리킬까?
오늘 Post(게시물)는 작성자를 author 로 가리켰어요. 한 방향이죠. 그런데 반대로 Member(회원)도 "내가 쓴 글 목록"(writtenPosts)을 들고 있으면, 회원에서 글로도 건너갈 수 있어요. 이렇게 양쪽이 서로를 가리키면(양방향) 어느 쪽에서 출발하든 편하게 따라갈 수 있죠. 하지만 함정이 하나 있어요. 글 작성자를 바꿨는데 회원 쪽 글 목록을 같이 안 고치면, 둘이 서로 다른 이야기를 하게 돼요. 양쪽을 항상 맞춰주는 일이 추가로 생기는 거죠. 어떤 관계는 양쪽에서 다 가리키게 두고, 어떤 관계는 한쪽만 가리키게 둘지 — 그 기준을 어떻게 세우면 좋을지 고민해보세요.
주제 2 — 관계를 별도 객체(Follow)로 승격할까, 그냥 목록 항목으로 둘까?
오늘 우리는 팔로우를 두 가지 방식으로 다뤘어요. 하나는 Member 의 following 배열에 상대 회원을 그냥 담는 방식(목록 항목), 또 하나는 Follow 라는 독립된 객체로 떼어내는 방식(관계 객체)이었죠. 단순히 "누구를 팔로우하는가" 만 알면 되는 경우엔 배열에 담는 쪽이 가볍고 충분해요. 하지만 "언제 팔로우했는지", "알림을 켜뒀는지" 같은 정보를 관계 자체에 붙이고 싶어지면, 그제야 별도 객체가 필요해지죠. 언제 관계를 객체로 승격하고, 언제 그냥 목록 항목으로 두는 게 적당할지 자기만의 기준을 떠올려보세요.
주제 3 — 같은 정보를 두 형태로(authorName과 author) 들고 다녀도 될까?
오늘 Post 는 작성자를 두 가지로 들고 있었어요. 이름만 베껴둔 authorName(문자열)과 실제 회원을 가리키는 author(객체)였죠. 둘 다 같은 작성자를 가리키니, 어떻게 보면 정보가 한 번 더 들어 있는 셈이에요. 이런 중복은 위험할 수 있어요. 작성자가 이름을 바꿨는데 author 쪽만 따라가고 authorName 은 옛 이름 그대로면, 둘이 어긋나버리니까요. 그런데도 우리가 둘 다 둔 이유는, 이름만 빠르게 보고 싶을 때가 많아서였죠. "편의를 위한 중복" 을 어디까지 허용하고, 어디서부터는 "하나만 진짜로 두고 나머지는 따라가서 읽자" 로 선을 그을지 — 그 트레이드오프를 한번 따져보세요.
✅ 예시 답안정답 보기
아래 답안은 "정답 하나" 가 아니라 오늘 배운 패턴(Post 가 작성자를 객체로 가리키던 방식, following 배열, Follow 관계 객체)을 그대로 가져와 푼 모범 사례 중 하나예요. 여러분 코드가 이것과 글자까지 똑같지 않아도 괜찮아요. 핵심 패턴을 제대로 짚었는지를 위주로 비교해보세요.
과제 1 예시답안 — 댓글에 작성자를 Member 객체로 연결하기
핵심 접근
Step 2에서 Post 가 작성자를 String authorName(이름)과 Member author(객체) 두 가지로 가리켰던 패턴을 댓글에 그대로 옮기는 게 핵심이에요. 객체를 받는 생성자 안에서 this.author = commenter.getUsername(); 으로 이름을 베껴두면, "객체로 따라가기" 와 "이름만 빠르게 보기" 가 늘 같은 사람을 가리켜요. 기존 이름만 받는 생성자를 지우지 않고 객체 받는 생성자를 하나 더 두면(Day 6 오버로딩), 옛 방식과 새 방식이 둘 다 살아 있게 돼요.
오늘 우리 Comment 는 불변(필드를 바꾸지 않는) 형태로 다뤘기 때문에, 답안에서는 기존 Comment 를 직접 뜯어고치지 않고 MemberAwareComment 라는 별도 클래스로 떼어 만들었어요. 여러분은 기존 Comment 에 바로 필드를 추가하셔도 됩니다 — 둘 다 맞아요.
예시 구현
// com/instagram/javabasic/solution/day16/MemberAwareComment.java
public class MemberAwareComment {
private String author; // 작성자 이름 — 글자 그대로 보관
private String text;
private int likeCount;
private Member commenter; // 작성자를 Member 객체로 직접 가리켜요(참조)
// 예전처럼 이름만 받는 생성자 — Member 연결 없이도 만들 수 있어요
public MemberAwareComment(String author, String text, int likeCount) {
this.author = author;
this.text = text;
this.likeCount = likeCount;
}
// Member 객체를 받는 생성자 — 도메인 연결의 핵심
// 객체를 받는 순간 그 사람의 이름을 author 에도 똑같이 베껴둬요
public MemberAwareComment(Member commenter, String text, int likeCount) {
this.commenter = commenter;
this.author = commenter.getUsername();
this.text = text;
this.likeCount = likeCount;
}
public String getAuthor() {
return author;
}
// 작성자 객체를 그대로 돌려줘요 — 이걸로 작성자의 점수·등급까지 따라가요
public Member getCommenter() {
return commenter;
}
@Override
public String toString() {
return "@" + author + ": " + text + " (좋아요 " + likeCount + ")";
}
}
실행하면 comment.getCommenter().grade() 처럼 댓글에서 작성자 객체를 따라가 등급까지 꺼낼 수 있어요.
// com/instagram/javabasic/solution/day16/Day16SolutionMain.java (발췌)
Member jaehoon = new Member("jaehoon", 0, 0, 7, 0);
MemberAwareComment comment = new MemberAwareComment(jaehoon, "멋진 사진이네요!", 0);
System.out.println("댓글 작성자 이름: " + comment.getAuthor());
System.out.println("작성자 객체 따라가기 — 등급: " + comment.getCommenter().grade());
채점 포인트
| 포인트 | 무엇을 봐야 하는가 | 배점 가중 |
|---|---|---|
| 객체 받는 생성자 추가 | Member 를 받는 생성자가 있고, 그 안에서 commenter 필드를 채우는가 |
상 |
| 이름 동기화 | 객체 생성자 안에서 this.author = commenter.getUsername(); 으로 이름을 베껴두는가 |
상 |
| 기존 생성자 보존(오버로딩) | 이름만 받는 옛 생성자를 지우지 않고 둘 다 살려뒀는가 | 중 |
| getCommenter 제공 | 작성자 객체를 그대로 돌려주는 getter 가 있고, 이를 통해 grade() 까지 따라가는가 |
중 |
| main 검증 | 회원 하나로 댓글을 만들고 작성자 등급까지 출력해 동작을 확인했는가 | 하 |
흔한 실수
- 이름만 받는 생성자로만 만들고
commenter필드는 안 채움 →getCommenter()가null을 돌려줘서.grade()호출 시NullPointerException. 객체를 받는 생성자가 있어야 작성자 객체가 실제로 연결돼요. - 객체 생성자에서
this.author = commenter.getUsername();줄을 빠뜨림 →commenter는 채워졌는데author는null. 이름만 빠르게 볼 때 빈 값이 나와요. Step 2Post패턴의 핵심이 바로 이 한 줄이에요. - 기존 이름 생성자를 지우고 객체 생성자로 덮어씀 → 틀린 건 아니지만, 요구사항은 둘 다 살려서 오버로딩을 연습하는 것이었어요.
실무 개선 포인트 (심화)
- 실무에서는 작성자가 없는 댓글(
commenter == null)이 들어올 수 있는지 자체를 막는 경우가 많아요. 객체 생성자만 남기고 이름 생성자를 없애 "작성자 객체는 반드시 있어야 한다" 를 강제하는 설계도 한 갈래예요. 지금은 옛 방식 호환을 위해 둘 다 뒀지만, 신규 코드만 있다면 객체 하나로 통일하는 쪽이 더 안전해요. - 나중에 다룰 컬렉션을 쓰면 한 게시물에 달린 댓글들을 묶어서 들고 다니게 돼요. 그때 각 댓글이 작성자 객체를 들고 있으면, 댓글 목록만으로 "이 글에 댓글 단 사람들의 평균 등급" 같은 집계까지 이어갈 수 있어요.
과제 2 예시답안 — 회원에게 "나를 팔로우하는 사람들"(팔로워) 배열 달기
핵심 접근
오늘 만든 following(내가 팔로우하는 사람들) 배열 + followingCount + follow 묶음을, 방향만 반대로 돌려 "나를 팔로우하는 사람들" 로 복제하는 게 핵심이에요. 기존 Member 를 건드리지 않고 기능만 덧붙이고 싶으니, Member 를 상속(extends)해서 새 배열을 더해요. 부모 Member 의 다섯 필드가 모두 private 이라 자식에서 직접 못 건드리고, super(...) 로 부모 생성자에 위임해 채워야 한다는 점이 이 과제의 진짜 포인트예요.
예시 구현
// com/instagram/javabasic/solution/day16/FollowableMember.java
public class FollowableMember extends Member {
private static final int CAPACITY = 16;
// 나를 팔로우하는 사람들 — 배열 + 카운터(1:N)
// 부모 Member 에 이미 int followers(팔로워 "수") 가 있어서, 혼동을 피하려고
// 배열 이름은 followerMembers 로 따로 둬요. (사람들의 묶음 vs 단순 숫자)
private Member[] followerMembers = new Member[CAPACITY];
private int followerCount = 0;
// 부모 필드는 private 이라, super(...) 통로로만 채워요
public FollowableMember(String username, int followers, int posts, int mutualFriends, int daysActive) {
super(username, followers, posts, mutualFriends, daysActive);
}
// 나를 팔로우하는 사람 하나를 묶음에 더해요. 자리가 차면 넘어가요.
// 기존 Member.follow 와 방향만 반대일 뿐 구조는 똑같아요.
public void addFollower(Member m) {
if (followerCount >= followerMembers.length) {
System.out.println("팔로워 묶음이 가득 찼어요. 더 담지 않아요.");
return;
}
followerMembers[followerCount] = m;
followerCount++;
}
public int getFollowerCount() {
return followerCount;
}
public Member getFollower(int index) {
return followerMembers[index];
}
}
여기서 한 가지 헷갈리기 쉬운 점 — 부모에서 물려받은 getFollowers() 는 처음 만들 때 넣은 팔로워 "수"(단순 숫자)고, 우리가 새로 만든 getFollowerCount() 는 배열에 실제로 담긴 사람 "묶음" 의 크기예요. 둘은 완전히 별개라서, 숫자 1200 으로 만들어도 배열에 두 명만 담으면 getFollowerCount() 는 2 예요.
// Day16SolutionMain.java (발췌)
FollowableMember star = new FollowableMember("star", 1200, 42, 3, 90);
star.addFollower(new Member("a", 0, 0, 0, 0));
star.addFollower(new Member("b", 0, 0, 0, 0));
System.out.println("@star 의 팔로워 수: " + star.getFollowerCount()); // 2 (숫자 1200 과 별개)
for (int i = 0; i < star.getFollowerCount(); i++) {
System.out.println(" 팔로워: @" + star.getFollower(i).getUsername());
}
채점 포인트
| 포인트 | 무엇을 봐야 하는가 | 배점 가중 |
|---|---|---|
| 배열 + 카운터 1:N | Member[] 배열과 int 카운터 짝으로 팔로워를 모으는가 (List 안 씀) |
상 |
| 용량 검사 | addFollower 안에서 if (카운터 >= 배열.length) 로 넘침을 먼저 막는가 |
상 |
| super 위임 | 부모 필드가 private 임을 이해하고 super(...) 로 초기화했는가 |
상 |
| 이름 충돌 회피 | 숫자 팔로워 수와 사람 묶음을 별도 이름으로 구분했는가 | 중 |
| main 검증 | 팔로워 두세 명 담고 수·이름을 for 로 출력했는가 |
하 |
흔한 실수
- 자식에서
this.username = ...처럼 부모 필드를 직접 채우려다 컴파일 에러 → 부모 다섯 필드가 모두private이라 자식은 손댈 수 없어요.super(...)로 위임하는 게 유일한 통로예요. 이 에러를 직접 만나봐야 "private 의 벽" 이 몸에 새겨져요. - 용량 검사
if (followerCount >= followerMembers.length)줄을 빠뜨림 → 16명을 넘기는 순간 배열 칸을 넘어가ArrayIndexOutOfBoundsException. Step 3follow가 가진 안전선을 그대로 가져와야 해요. - 새 배열 이름을
followers로 지어 부모의int followers와 헷갈림 → 컴파일은 되더라도 "숫자 수" 와 "사람 묶음" 이 머릿속에서 섞여요. 답안처럼followerMembers로 이름을 갈라두면 혼동이 줄어요.
실무 개선 포인트 (심화)
- 지금은
Member를 상속해서 팔로워 기능을 덧붙였지만, 실무에서는 "회원" 이라는 하나의 개념을 굳이 둘로 쪼개지 않고 원래Member안에following과followers배열을 나란히 두는 쪽이 더 흔해요. 상속은 "이 회원만 특별히 팔로워를 센다" 같은 진짜 분화가 있을 때 쓰는 게 자연스러워요. 이번 과제는 기존Member를 건드리지 않고 연습하려고 상속을 택한 거예요. - 팔로워가 16명을 넘으면 그냥 버리는 지금 방식은 연습용이에요. 나중에 컬렉션을 배우면 가득 차도 알아서 늘어나는 자료구조로 바꿔, 용량 검사 코드 자체가 사라져요.
과제 3 예시답안 — 차단(Block) 관계를 객체로 만들기
핵심 접근
Step 6의 Follow 클래스(follower/followee 두 사람을 final 로 잠그고, toString 으로 관계를 한 줄로 보여주던 그 형태)를 이름만 blocker/blocked 로 바꿔 그대로 본뜨는 게 핵심이에요. "누가 누구를 차단했다" 는 두 회원을 잇는 연결 한 건이고, 그 연결 자체를 객체로 승격하면 나중에 "언제·왜 차단했는지" 같은 정보를 이 객체에 자연스럽게 붙일 수 있어요. 한 번 맺어진 차단 관계는 바뀌지 않으니 두 필드를 final 로 잠가 불변으로 만드는 게 포인트예요.
예시 구현
// com/instagram/javabasic/solution/day16/Block.java
public class Block {
private final Member blocker; // 차단을 거는 사람
private final Member blocked; // 차단을 당하는 사람
// 두 사람을 받아 차단 관계를 완성해요. 한 번 정해지면 못 바꿔요.
public Block(Member blocker, Member blocked) {
this.blocker = blocker;
this.blocked = blocked;
}
public Member getBlocker() {
return blocker;
}
public Member getBlocked() {
return blocked;
}
// 관계를 한눈에 — "@차단한사람 ⊘ @차단당한사람" 형식
@Override
public String toString() {
return "@" + blocker.getUsername() + " ⊘ @" + blocked.getUsername();
}
}
toString 안에서 회원 이름은 객체를 따라가(blocker.getUsername()) 꺼내요. Block 자신은 이름 문자열을 따로 들고 있지 않고, 두 회원 객체만 가리킨 채 필요할 때 따라가 읽는 거예요.
// Day16SolutionMain.java (발췌)
Block block = new Block(jaehoon, new Member("spammer", 0, 0, 0, 0));
System.out.println("차단 관계: " + block); // @jaehoon ⊘ @spammer
채점 포인트
| 포인트 | 무엇을 봐야 하는가 | 배점 가중 |
|---|---|---|
| 관계를 객체로 승격 | 두 회원을 잇는 연결을 독립된 Block 클래스로 떼어냈는가 |
상 |
| final 불변 | 두 필드를 final 로 잠가 한 번 정하면 못 바꾸게 했는가 |
상 |
| toString 오버라이딩 | @Override 로 "@blocker ⊘ @blocked" 형식을 만들었는가 |
중 |
| 객체 따라가 이름 꺼내기 | toString 에서 이름을 따로 저장하지 않고 getUsername() 으로 따라가는가 |
중 |
| main 검증 | 회원 둘로 Block 을 만들고 println 으로 한 줄 출력을 확인했는가 |
하 |
흔한 실수
- 필드에
final을 안 붙임 → 컴파일은 되지만 "한 번 맺으면 안 바뀌는 관계" 라는 의도가 코드에 안 드러나요. 차단한 사람·당한 사람이 도중에 바뀌면 그건 더 이상 같은 차단 건이 아니에요.final로 그 의도를 못 박는 게 이 과제의 설계 감각이에요. Block안에 회원 이름까지String으로 또 저장 → 이름이 바뀌면 어긋나는 중복이 생겨요(주제 3의 트레이드오프와 같은 함정). 두 회원 객체만 가리키고 이름은 따라가 읽으면 늘 최신 이름이 나와요.toString에@Override를 빼먹음 → 동작은 같지만, 부모 메서드를 덮어쓰는 의도를 컴파일러가 검사하지 못해요. 메서드 이름 오타가 있어도 그냥 새 메서드로 만들어져 버려요.
실무 개선 포인트 (심화)
- 관계를 객체로 승격하면 그 위에 정보를 붙일 자리가 생겨요. 실무 차단 객체에는 보통
차단 시각, 신고 기반이라면사유같은 필드가 붙어요. 단순히 "차단했다/안 했다" 만 필요하면 굳이 객체로 안 만들고 회원이 차단 대상 배열만 들고 있어도 충분해요 — 주제 2의 "목록 항목 vs 관계 객체" 선택과 똑같은 갈림길이에요. Follow와Block이 "두 회원 + final + toString" 이라는 똑같은 뼈대를 공유하죠. 나중에 이런 공통 뼈대를 부모 클래스나 인터페이스(Day 13)로 묶어 중복을 줄이는 설계로 발전시킬 수 있어요. 지금은 각자 따로 두고 패턴을 눈에 익히는 단계예요.
생각해볼 주제 1 예시답안 — 양방향으로 서로 가리킬까, 한 방향만 가리킬까?
[문제 상황 요약]
오늘 Post(게시물)는 작성자를 author 로 한 방향으로만 가리켰어요. 그런데 Member(회원)도 "내가 쓴 글 목록"(writtenPosts)을 들고 있으면, 회원에서 글로도 건너갈 수 있어 양쪽이 서로를 가리키게 돼요. 양방향은 어느 쪽에서 출발하든 편하지만, 한쪽을 바꿨을 때 반대쪽도 같이 안 고치면 둘이 서로 다른 이야기를 하게 되는 함정이 따라와요. 어떤 관계를 양방향으로 두고 어떤 관계를 한 방향만 둘지, 그 기준을 어떻게 세울까요?
[튜터의 가이드 및 해설]
이 질문은 "탐색의 편의" 와 "둘을 늘 맞춰주는 비용" 사이의 트레이드오프를 묻는 거예요. 양방향으로 두면 글에서 작성자로도, 작성자에서 글 목록으로도 자유롭게 건너가요. 대신 글의 작성자를 바꾸면 옛 작성자의 글 목록에서 그 글을 빼고 새 작성자의 목록에 넣는 일을 손으로 같이 해줘야 해요. 이 동기화를 한 군데라도 빠뜨리면 데이터가 서로 어긋나기 시작해요.
Option A — 양방향(양쪽이 서로 가리킴): 장점은 탐색이 어디서 출발하든 한 번에 따라간다는 거예요. "이 글의 작성자" 도, "이 회원이 쓴 글들" 도 바로 손에 들어와요. 단점은 둘을 늘 짝 맞춰 갱신해야 한다는 점이에요. 추가·삭제·변경 때마다 양쪽을 모두 건드려야 하고, 한쪽만 고치면 데이터가 깨져요.
Option B — 한 방향(한쪽만 가리킴): 장점은 단순함이에요. 진실의 출처가 한 곳뿐이라 어긋날 일이 없어요. 단점은 반대 방향 탐색이 번거롭다는 거예요. "이 회원이 쓴 글들" 을 알려면 전체 글을 훑으며 작성자가 이 회원인지 일일이 비교해야 해요(전수 검사).
현업에서는 보통: "그 반대 방향 탐색이 실제로 자주 필요한가?" 를 먼저 물어요. 자주 쓰는 방향만 연결을 두는 게 기본이에요. 글에서 작성자로 가는 건 거의 항상 필요하니 그 방향은 꼭 두고, 회원에서 글 목록으로 가는 건 정말 자주 쓸 때만 양방향으로 올려요. 그리고 양방향을 둔다면 "양쪽을 한 번에 맞춰주는 메서드 하나" 를 만들어 그 안에서만 갱신하게 강제해요. 양쪽을 직접 따로 건드리는 코드를 흩어두는 순간 동기화 누락 버그가 시작되거든요.
🎯 면접관을 홀리는 핵심 멘트
"양방향 연관은 공짜가 아닙니다. 탐색 편의를 얻는 대신 양쪽을 늘 동기화해야 하는 비용이 따라붙죠. 그래서 저는 기본을 한 방향으로 두고, 반대 방향 탐색이 실제로 자주 필요할 때만 양방향으로 올립니다. 양방향을 둘 땐 양쪽을 한 곳에서만 갱신하도록 메서드로 묶어, 한쪽만 바뀌어 데이터가 어긋나는 사고를 원천 차단합니다."
생각해볼 주제 2 예시답안 — 관계를 별도 객체(Follow)로 승격할까, 목록 항목으로 둘까?
[문제 상황 요약]
오늘 팔로우를 두 가지로 다뤘어요. 하나는 Member 의 following 배열에 상대 회원을 그냥 담는 방식(목록 항목), 또 하나는 Follow 라는 독립 객체로 떼어내는 방식(관계 객체)이었죠. "누구를 팔로우하는가" 만 알면 될 땐 배열에 담는 쪽이 가볍고 충분해요. 하지만 "언제 팔로우했는지", "알림을 켜뒀는지" 같은 정보를 관계 자체에 붙이고 싶어지면 그제야 별도 객체가 필요해져요. 언제 객체로 승격하고 언제 목록 항목으로 둘까요?
[튜터의 가이드 및 해설]
판단 기준은 딱 하나예요 — "그 관계 자체에 붙일 정보가 있는가?" 두 사람이 연결됐다는 사실만 알면 충분하다면 목록 항목으로 충분하고, 그 연결에 시각·설정·상태 같은 살이 붙기 시작하면 관계를 객체로 올릴 때예요.
Option A — 목록 항목(배열에 상대 회원만 담기): 장점은 가볍고 직관적이라는 거예요. 코드도 적고, "팔로우한다 = 배열에 더한다" 가 한눈에 들어와요. 단점은 관계에 정보를 못 붙인다는 점이에요. "언제 팔로우했는지" 를 담을 자리가 어디에도 없어요. 회원 객체에 붙일 수도 없죠 — 그건 회원의 정보가 아니라 두 사람 사이의 정보니까요.
Option B — 관계 객체(Follow 로 승격): 장점은 관계에 정보를 얼마든지 붙일 수 있다는 거예요. followedAt(시각), notificationOn(알림 설정) 같은 필드를 Follow 안에 자연스럽게 둘 수 있어요. 단점은 객체가 하나 더 늘어 관리할 게 많아진다는 점이에요. 단순한 팔로우에는 과한 무게예요.
현업에서는 보통: "지금 당장 관계에 붙일 정보가 없으면 목록 항목으로 시작" 해요. 그러다 "이 관계에 시각이나 상태를 붙여야겠다" 는 요구가 생기는 순간 객체로 승격해요. 미리 다 객체로 만들면 오히려 짐이 돼요. 다만 차단·신고처럼 "왜·언제" 가 거의 항상 따라붙는 관계는 처음부터 객체로 두는 게 보통이에요. 과제 3의 Block 을 객체로 만든 것도 같은 맥락이에요 — 차단엔 사유와 시각이 자연스럽게 따라오니까요.
🎯 면접관을 홀리는 핵심 멘트
"관계를 객체로 올릴지는 '그 관계 자체에 붙일 정보가 있느냐' 로 갈립니다. 연결됐다는 사실만 필요하면 목록 항목으로 가볍게 가고, 시각·상태·설정처럼 두 사람 어느 쪽에도 속하지 않는 정보가 생기면 그때 관계를 객체로 승격합니다. 미리 다 객체로 만드는 건 과설계라, 정보가 붙는 시점에 올리는 걸 원칙으로 둡니다."
생각해볼 주제 3 예시답안 — 같은 정보를 두 형태로(authorName과 author) 들고 다녀도 될까?
[문제 상황 요약]
오늘 Post 는 작성자를 두 가지로 들고 있었어요. 이름만 베껴둔 authorName(문자열)과 실제 회원을 가리키는 author(객체). 둘 다 같은 작성자를 가리키니 정보가 한 번 더 들어 있는 셈이에요. 이런 중복은 위험할 수 있어요 — 작성자가 이름을 바꿨는데 author 쪽만 따라가고 authorName 은 옛 이름 그대로면 둘이 어긋나니까요. 그런데도 둘 다 둔 이유는 이름만 빠르게 보고 싶을 때가 많아서였죠. "편의를 위한 중복" 을 어디까지 허용할까요?
[튜터의 가이드 및 해설]
이 질문은 "읽기 속도" 와 "데이터 정합성"(둘이 어긋나지 않음) 사이의 트레이드오프예요. 객체 하나만 두면 어긋날 일이 없지만, 이름 하나 보려고 매번 객체를 따라가야 해요. 이름을 따로 베껴두면 빠르게 읽지만, 원본 이름이 바뀌면 베껴둔 값이 옛것으로 남는 위험이 생겨요.
Option A — 객체 하나만 두기(author 만): 장점은 진실의 출처가 하나뿐이라 절대 어긋나지 않는다는 거예요. 이름을 바꿔도 author.getUsername() 은 늘 최신이에요. 단점은 이름 하나 볼 때마다 객체를 따라가야 한다는 점이에요. 작성자 객체가 사라졌거나 비어 있으면 이름조차 못 읽는 상황도 생겨요.
Option B — 이름을 따로 베껴두기(authorName + author): 장점은 이름을 즉시 읽을 수 있다는 거예요. 객체를 안 따라가도 되고, 객체가 없어도 이름은 남아요. 단점은 원본 이름이 바뀌면 베껴둔 값과 어긋날 위험이에요. 이 중복을 안전하게 쓰려면 "베껴두는 건 만들 때 딱 한 번, 이후엔 안 바꾼다" 같은 규칙이 필요해요.
현업에서는 보통: 둘 다 써요 — 단, 의미를 갈라서요. 한쪽은 "지금 살아있는 최신 값" 으로 객체를 따라가 읽고, 다른 한쪽은 "그 시점의 스냅샷(찍어둔 사진)" 으로 일부러 안 바꾸는 거예요. 예를 들어 "댓글을 달 당시의 작성자 이름" 은 작성자가 나중에 이름을 바꿔도 그대로 남아 있는 게 오히려 맞아요(기록의 의미). 반대로 "지금 이 글의 작성자" 를 보여줄 땐 객체를 따라가 최신 이름을 읽어야 하고요. 즉 중복이 무조건 나쁜 게 아니라, 그 베껴둔 값이 "최신을 따라가야 하는 값" 인지 "그때를 박제한 값" 인지를 분명히 정하는 게 핵심이에요. 정하지 않은 채 어쩌다 둘 다 들고 있으면 그게 진짜 버그의 씨앗이에요.
🎯 면접관을 홀리는 핵심 멘트
"중복 자체가 나쁜 게 아니라, 그 중복의 의미를 정하지 않은 게 나쁩니다. 베껴둔 값이 원본을 따라가야 하는 최신값인지, 아니면 그 시점을 박제한 스냅샷인지를 먼저 정합니다. 스냅샷이라면 일부러 안 바꾸는 게 맞고, 최신값이라면 애초에 베끼지 말고 객체를 따라가 읽습니다. 둘 다 들고 있으면서 의미를 안 정해두면, 그게 데이터가 어긋나는 버그의 출발점입니다."