Day 40 — 생성 패턴: 객체를 똑똑하게 만들어내기
목차 17
지난 시간 Day 39 까지 우리는 자바의 핵심 문법과 라이브러리를 두루 익혔어요. 변수와 반복문에서 시작해 클래스·상속·인터페이스로 객체를 설계했고, 컬렉션·제네릭·예외로 데이터를 안전하게 다뤘고, 람다·Stream 으로 선언적으로 처리했고, 멀티스레드와 파일 입출력까지 손에 넣었죠. 이제 자바로 무언가를 만들 도구는 거의 다 갖춘 셈이에요.
그런데 같은 기능이라도 어떻게 짜느냐 에 따라 코드의 운명이 갈려요. 어떤 코드는 나중에 요구사항이 바뀌어도 한 군데만 고치면 되는데, 어떤 코드는 한 줄 바꾸려다 열 군데가 줄줄이 무너져요. 오늘부터 들어가는 Phase 6 — 설계와 깊이 는 바로 이 차이를 다뤄요. 같은 동작을 더 유연하고 고치기 쉬운 구조로 짜는 법이죠.
그 첫걸음이 디자인 패턴(design pattern) 이에요. 디자인 패턴은 앞서간 개발자들이 "이런 상황에선 이렇게 짜두면 두고두고 편하더라" 를 정리해둔 검증된 설계 레시피예요. 중요한 건 이름을 외우는 게 아니에요. 각 패턴이 어떤 불편을 풀어주는지 를 이해하는 거예요. 오늘은 그중에서도 객체를 만들어내는 순간 을 다루는 생성 패턴(creational pattern) 네 가지를 만나봐요. 시작해봐요!
🎯 학습 목표
- 디자인 패턴이 무엇인지 — "검증된 설계 레시피" 로 이해하고, 외우기보다 동기를 잡아요.
- Singleton — 앱 전체에 딱 하나만 있어야 하는 객체를 안전하게 만드는 법 (Enum 방식).
- Builder — 길고 헷갈리는 생성자의 고통을, 재료에 이름표를 붙여 쌓는 방식으로 푸는 법.
- Factory Method — 무엇을
new할지 한 곳에서 결정해, 종류가 늘어도 흔들리지 않는 법. - Abstract Factory — 서로 어울려야 하는 객체 한 벌을 통째로, 일관되게 만드는 법.
- 패턴은 만능이 아니라는 것 — 언제 쓰고 언제 과한지 가늠하는 눈.
오늘의 로드맵
- Step 1 — 딱 하나만 있어야 하는 객체, Singleton.
- Step 2 — 생성자가 자꾸 길어지는 고통 (점층적 생성자의 한계).
- Step 3 — 재료에 이름표 붙여 쌓고
build(), Builder. - Step 4 — 무엇을 만들지 한 곳에서 결정, Factory Method.
- Step 5 — 어울리는 한 벌을 통째로, Abstract Factory.
- Step 6 — 네 패턴 한자리 비교, 그리고 패턴은 도구일 뿐.
Step 1: 딱 하나만 있어야 하는 객체, Singleton
게시물을 올릴 때마다 새 번호(ID)를 하나씩 붙인다고 해봐요. 1번 게시물, 2번 게시물, 3번 게시물… 이 번호를 발급해주는 "발급기" 가 하나 필요하겠죠. 그런데 만약 이 발급기를 여기서도 new, 저기서도 new 해서 여러 개 만들면 어떻게 될까요?
❌ 발급기가 여러 개일 때
발급기 A 가 발급: 1, 2, 3 ...
발급기 B 가 발급: 1, 2, 3 ...
→ 서로 모르니 똑같이 1번부터 세요. 1번 게시물이 둘! (번호 중복)
✅ 발급기가 딱 하나일 때 (Singleton)
유일한 발급기가 발급: 1, 2, 3, 4, 5 ...
→ 한 곳에서만 세니 번호가 절대 안 겹쳐요.
이렇게 앱 전체에 단 하나만 존재해야 하는 객체 가 있어요. 번호 발급기, 앱 전체 설정값, 공용 로그 기록기 같은 것들이요. 이런 "단 하나뿐인 객체" 를 만드는 패턴을 Singleton(싱글턴) 이라고 불러요.
자바에는 이걸 만드는 가장 간단하고 안전한 방법이 있어요. 바로 enum 이에요. Day 15 에서 배운 그 enum 맞아요. enum 상수는 자바가 프로그램이 켜질 때 딱 하나만 만들어두고, 여러 곳에서 동시에 불러도 안전하게 같은 걸 돌려줘요. 우리가 직접 "하나만 만들어지게" 신경 쓸 필요가 없죠.
// com/instagram/javabasic/design/creational/IdGenerator.java
public enum IdGenerator {
// 단 하나뿐인 인스턴스 — 이 줄이 곧 "유일한 발급기" 예요.
INSTANCE;
private long lastId = 0;
public synchronized long nextId() {
lastId++;
return lastId;
}
}
쓸 때는 어디서든 IdGenerator.INSTANCE 로 불러요. 몇 번을 불러도 항상 같은 발급기예요.
long id1 = IdGenerator.INSTANCE.nextId(); // 1
long id2 = IdGenerator.INSTANCE.nextId(); // 2
long id3 = IdGenerator.INSTANCE.nextId(); // 3
nextId() 앞에 붙은 synchronized 는 Day 34 에서 배운 그 빗장이에요. 여러 작업이 동시에 번호를 받아가도 숫자가 꼬이지 않게 막아줘요. 발급기가 하나뿐이니, 그 하나를 동시에 쓰는 상황만 조심하면 되는 거죠.
🙋 학생 질문 — "그냥 static 변수 하나 두면 되지 않나요? 왜 굳이 enum 이에요?"
좋은 질문이에요. 사실 private 생성자에 static 인스턴스를 두는 방식으로도 싱글턴을 만들 수 있어요. 옛날엔 그렇게 많이 했고요.
문제는 그 방식이 은근히 함정이 많다는 거예요. 여러 스레드가 동시에 처음 만들려 하면 실수로 두 개가 생길 수 있고, 객체를 파일로 저장했다 불러오는(역직렬화) 과정에서 또 하나가 더 생기기도 해요. 이런 구멍을 다 막으려면 코드가 꽤 까다로워져요.
enum 은 이 모든 걸 자바가 알아서 막아줘요. 단 하나만 만들어지는 것도, 동시에 불러도 안전한 것도, 저장했다 불러와도 그대로인 것도 전부 보장돼요. 그래서 "싱글턴이 필요하면 enum 으로" 가 현대 자바의 권장 방식이에요. 가장 적게 쓰고 가장 안전하거든요.
Step 2: 생성자가 자꾸 길어지는 고통
Day 8~9 에서 만든 Member 클래스를 다시 떠올려봐요. 회원 한 명을 만드는 생성자가 여러 개 있었죠. 상황마다 받는 값이 달라서, 생성자를 이렇게 여러 벌 만들어뒀어요.
// com/instagram/javabasic/domain/member/Member.java
public Member() { ... }
public Member(String username, int followers, int posts, int mutualFriends, int daysActive) { ... }
public Member(String username, String email) { ... }
이렇게 같은 클래스에 생성자를 여러 개 겹쳐 두는 걸 점층적 생성자(telescoping constructor) 라고 해요. "조금씩 늘려가며 쌓은 생성자" 라는 뜻이에요. 처음엔 편해 보이는데, 두 가지 고통이 따라와요.
첫째, 숫자만 늘어놓으면 무슨 뜻인지 알 수가 없어요. 다섯 개짜리 생성자를 실제로 불러볼게요.
new Member("jaehoon", 1200, 42, 8, 90)
│ │ │ └ 90 은 활동 일수? 90일?
│ │ └ 8 은 함께 아는 친구?
│ └ 42 는 게시물 수?
└ 1200 은 팔로워 수?
숫자 네 개가 각각 뭘 뜻하는지, 코드만 봐선 알 수 없어요
순서를 하나라도 헷갈려서 게시물 수와 팔로워 수를 바꿔 넣어도, 컴파일러는 둘 다 int 라서 아무 말 없이 통과시켜요. 조용한 버그가 숨어들기 딱 좋죠.
둘째, 생성자 종류를 무한정 늘릴 수 없어요. "이름·이메일·팔로워만 받는 생성자", "이름·게시물만 받는 생성자" … 선택 항목이 늘어날수록 조합은 폭발해요. 그렇다고 모든 필드를 setter 로 하나씩 채우면, 객체가 잠깐 "절반만 완성된 상태" 로 떠돌게 되고, 한 번 만든 값이 언제든 바뀔 수 있어 불안해져요.
그럼 이 고통을 어떻게 풀까요? 핵심 아이디어는 이거예요. 재료를 한꺼번에 순서대로 밀어 넣지 말고, 하나씩 이름표를 붙여 천천히 쌓은 다음, 마지막에 "완성!" 하고 굳히자. 다음 Step 에서 그 방법을 만들어봐요.
Step 3: 재료에 이름표 붙여 쌓고 build(), Builder
Step 2 의 고통을 푸는 패턴이 Builder(빌더) 예요. 이름표를 붙여 재료를 쌓아 올리는 "조립 도우미" 를 따로 두는 방식이죠. 프로필 카드를 만드는 ProfileCard 로 직접 봐요. 사용자 이름은 꼭 있어야 하고(필수), 소개글·웹사이트·인증 배지·팔로워 수는 있을 수도 없을 수도 있어요(선택).
[필수] username ─┐
[선택] bio ─┤
[선택] website ─┼──▶ build() ──▶ 완성된 ProfileCard (불변)
[선택] verified ─┤
[선택] follower ─┘
쓰는 쪽 코드를 먼저 볼게요. 한눈에 무엇을 채우는지 보이죠?
ProfileCard card = ProfileCard.builder("jaehoon") // 필수: 이름으로 시작
.bio("사진 찍는 걸 좋아해요 📷") // 선택: 소개글
.verified(true) // 선택: 인증 배지
.followerCount(1200) // 선택: 팔로워
.build(); // 완성!
new Member("jaehoon", 1200, 42, 8, 90) 와 비교해보세요. 어떤 값이 무엇인지 메서드 이름이 그대로 알려줘요. 순서를 바꿔도, 필요 없는 선택 항목은 빼도 괜찮아요. 그럼 이 ProfileCard 가 어떻게 생겼는지 안을 들여다봐요.
// com/instagram/javabasic/design/creational/ProfileCard.java
public class ProfileCard {
private final String username; // 필수
private final String bio; // 선택
private final boolean verified; // 선택 (website, followerCount 생략)
// 생성자는 private — 바깥에서 new 로 직접 못 만들게 막고, 오직 Builder 로만 만들게 해요.
private ProfileCard(Builder builder) {
this.username = builder.username;
this.bio = builder.bio;
this.verified = builder.verified;
}
// 조립을 시작하는 입구 — 필수 값으로 시작해요.
public static Builder builder(String username) {
return new Builder(username);
}
}
필드가 전부 final 이라, 한 번 완성되면 값이 바뀌지 않아요(불변). Day 29 에서 불변 객체가 왜 안전한지 배웠죠. 이제 핵심인 Builder 안쪽이에요.
// ProfileCard 안의 정적 중첩 클래스
public static class Builder {
private final String username; // 필수 — 입구에서 미리 받아요
private String bio = ""; // 선택 — 기본값을 미리 둬요
private boolean verified = false;
private Builder(String username) {
this.username = username;
}
public Builder bio(String bio) {
this.bio = bio;
return this; // ← 자기 자신을 돌려줘서 점을 이어 찍을 수 있어요
}
public ProfileCard build() {
return new ProfileCard(this);
}
}
비밀은 return this 한 줄이에요. 값을 채운 뒤 자기 자신을 돌려주니까, .bio(...) 가 끝나면 또 빌더가 손에 남아 .verified(...) 를 바로 이어 부를 수 있어요. 이렇게 점을 줄줄이 이어 쓰는 방식을 흔히 "체이닝" 이라고 해요. 그리고 선택 항목엔 미리 기본값(bio = "")을 둬서, 안 넣으면 그 값이 그대로 쓰여요. 마지막 build() 가 쌓인 재료로 완성된 카드를 만들어 굳히죠.
🙋 학생 질문 — "필드를 그냥 setter 로 채우면 안 되나요? 더 간단해 보이는데요."
setter 로도 값을 채울 수 있어요. 하지만 두 가지가 아쉬워요.
하나는 불변이 깨져요. setter 가 열려 있으면 카드를 만든 뒤에도 누군가 card.setUsername("다른사람") 으로 언제든 바꿀 수 있어요. "한 번 만들면 안 바뀐다" 는 안전함이 사라지죠.
다른 하나는 반쯤 만들어진 객체가 떠돌아요. new ProfileCard() 직후, setUsername 을 부르기 전까지 그 카드는 이름이 비어 있는 미완성 상태예요. 누가 그 사이에 카드를 써버리면 문제가 생기죠.
Builder 는 이 둘을 다 막아요. 재료를 다 쌓을 때까진 ProfileCard 자체가 아예 안 만들어지고, build() 가 불리는 순간 완성된 불변 객체가 딱 한 번에 태어나거든요. "조립 중" 과 "완성품" 을 깔끔하게 나누는 거예요.
참고로, 선택 항목이 한두 개뿐이라면 Day 29 의 record 로도 충분히 깔끔해요. 선택 항목이 많아서 생성자가 길고 헷갈려질 때, 그때 Builder 가 빛을 발해요.
Step 4: 무엇을 만들지 한 곳에서 결정, Factory Method
Day 12 에서 만든 콘텐츠 가족을 다시 불러와요. Content 라는 부모 아래 ImageContent(이미지)·VideoContent(영상)·TextContent(글) 세 자식이 있었죠. 그런데 사용자가 무언가를 올릴 때, 종류에 따라 알맞은 콘텐츠를 만들어야 해요. 이걸 콘텐츠를 쓰는 화면마다 직접 고르게 하면 어떻게 될까요?
❌ 쓰는 곳마다 직접 고르기
피드 화면이 직접: new ImageContent(...) / new VideoContent(...) / new TextContent(...)
프로필 화면도 직접: new ImageContent(...) / new VideoContent(...) / ...
→ 새 종류(예: 라이브 방송)가 생기면, 이걸 쓰는 모든 화면을 다 찾아 고쳐야 해요
✅ 공장 한 곳에 맡기기 (Factory Method)
모든 화면은 그냥: ContentFactory.create(종류, 작성자, 내용)
→ 어떤 클래스를 new 할지는 공장이 결정 → 새 종류가 생겨도 공장 한 곳만 고치면 끝
이렇게 무엇을 new 할지 결정하는 일을 한 곳(공장)으로 모으는 패턴을 Factory Method(팩토리 메서드) 라고 해요. 콘텐츠를 쓰는 쪽은 "이미지면 어느 클래스, 영상이면 어느 클래스" 를 일일이 알 필요 없이, 공장에 종류만 알려주면 돼요.
// com/instagram/javabasic/design/creational/ContentFactory.java
public class ContentFactory {
public enum ContentType {
IMAGE, VIDEO, TEXT
}
public static Content create(ContentType type, String author, String main) {
return switch (type) {
case IMAGE -> new ImageContent(author, 0, main);
case VIDEO -> new VideoContent(author, 0, main, DEFAULT_VIDEO_SECONDS);
case TEXT -> new TextContent(author, 0, main);
};
}
}
종류를 고르는 switch 는 Day 15 에서 배운 그 표현식이에요. 갓 올라온 콘텐츠라 좋아요 수는 0 에서 시작하고, main 은 종류마다 의미가 달라요(이미지·영상은 주소, 글은 본문). 쓸 때는 이렇게 한 줄이에요.
Content c = ContentFactory.create(ContentType.IMAGE, "jaehoon", "photo.jpg");
System.out.println(c.render()); // [이미지] jaehoon 님의 사진: photo.jpg (♥ 0)
여기서 진짜 이득은, 공장이 돌려주는 게 구체적인 ImageContent 가 아니라 부모 타입인 Content 라는 점이에요. 받는 쪽은 그게 이미지인지 영상인지 몰라도, Day 11 에서 배운 다형성 덕분에 render() 한 번이면 종류에 맞는 미리보기가 알아서 나와요. 만드는 것도, 쓰는 것도 구체 클래스에 묶이지 않는 거예요.
🙋 학생 질문 — "Builder 랑 Factory 랑 비슷해 보여요. 뭐가 다른 거예요?"
둘 다 객체를 만들어주지만, 푸는 고민이 달라요. 한 문장으로 가르면 이래요.
Builder 는 "어떻게 조립할까" 예요. 하나의 종류(예: ProfileCard)인데 채울 재료가 많고 복잡할 때, 그걸 이름표 붙여 차근차근 쌓는 데 집중해요.
Factory 는 "무엇을 만들까" 예요. 종류가 여러 개(이미지·영상·글)일 때, 그중 무엇을 new 할지 고르는 데 집중해요.
그래서 "재료가 많아 헷갈린다" 면 Builder, "종류가 여러 개라 고르기가 번진다" 면 Factory 예요. 실무에선 둘을 함께 쓰기도 해요. 공장이 종류를 고른 뒤, 그 안에서 빌더로 조립하는 식으로요.
Step 5: 어울리는 한 벌을 통째로, Abstract Factory
Factory Method 는 콘텐츠 "한 개" 를 만들었어요. 그런데 가끔은 서로 어울려야 하는 여러 개를 한 벌로 만들어야 할 때가 있어요. 인스타그램의 라이트 모드와 다크 모드를 떠올려봐요. 라이트 모드면 버튼도 밝고 아이콘도 그에 맞는 색이어야 하고, 다크 모드면 둘 다 어두운 쪽으로 맞춰져야 해요. 실수로 라이트 버튼에 다크 아이콘이 섞이면 화면이 어색해지죠.
이럴 때 쓰는 패턴이 Abstract Factory(추상 팩토리) 예요. 테마별로 "공장" 을 따로 두고, 그 공장이 버튼과 아이콘을 한 벌로 일관되게 찍어내게 하는 거예요. 먼저 공장의 약속(인터페이스)을 정해요.
// com/instagram/javabasic/design/creational/theme/ThemeFactory.java
public interface ThemeFactory {
ThemeButton createButton();
ThemeIcon createIcon();
}
그리고 테마마다 이 약속을 지키는 공장을 하나씩 만들어요.
// com/instagram/javabasic/design/creational/theme/LightThemeFactory.java
public class LightThemeFactory implements ThemeFactory {
@Override
public ThemeButton createButton() {
return new ThemeButton("화이트");
}
@Override
public ThemeIcon createIcon() {
return new ThemeIcon("블랙");
}
}
다크 공장(DarkThemeFactory)도 똑같은 약속을 지키되, 버튼은 "블랙", 아이콘은 "화이트" 로 만들어요. 두 공장을 나란히 보면 이래요.
라이트 공장 (LightThemeFactory)
├ createButton() → 화이트 배경 버튼
└ createIcon() → 블랙 아이콘
다크 공장 (DarkThemeFactory)
├ createButton() → 블랙 배경 버튼
└ createIcon() → 화이트 아이콘
이제 화면을 그리는 코드는 공장 하나만 받으면 돼요. 라이트인지 다크인지 신경 쓰지 않고요.
ThemeFactory factory = new DarkThemeFactory(); // 다크로 결정
ThemeButton button = factory.createButton(); // 블랙 배경 버튼
ThemeIcon icon = factory.createIcon(); // 화이트 아이콘
// 공장만 LightThemeFactory 로 바꾸면 버튼·아이콘이 통째로 라이트로 바뀌어요
핵심은 일관성 이에요. 한 공장에서 나온 버튼과 아이콘은 항상 같은 테마라, 색이 섞일 일이 없어요. 그리고 테마를 통째로 바꾸고 싶으면 공장 하나만 갈아끼우면 돼요. 화면 그리는 코드는 한 줄도 안 바뀌고요. "한 개를 고른다" 가 Factory Method 였다면, "어울리는 한 벌을 고른다" 가 Abstract Factory 예요.
Step 6: 네 패턴 한자리 비교, 그리고 패턴은 도구일 뿐
오늘 만난 네 패턴을 한자리에 모아봐요. 전부 "객체를 만드는 순간" 을 다루지만, 푸는 불편이 각자 달라요.
| 패턴 | 한 줄 정체 | 풀어주는 불편 | 오늘 만든 예 |
|---|---|---|---|
| Singleton | 앱에 딱 하나만 존재하는 객체 | 여러 개일 때 생기는 충돌(번호 중복 등) | IdGenerator |
| Builder | 재료에 이름표 붙여 쌓아 완성 | 길고 헷갈리는 생성자 | ProfileCard |
| Factory Method | 무엇을 new 할지 한 곳에서 결정 |
종류가 늘 때마다 곳곳을 수정 | ContentFactory |
| Abstract Factory | 어울리는 한 벌을 통째로 생성 | 서로 안 맞는 조합이 섞임 | ThemeFactory |
여기서 꼭 기억할 게 하나 있어요. 패턴은 망치가 아니라 연장통이에요. 그리고 세상 모든 게 못은 아니에요.
⚠️ 패턴을 배우면 자꾸 어디든 끼워 넣고 싶어져요. 하지만 값 두세 개짜리 단순한 객체에 굳이 Builder 를 붙이면, 안 그래도 짧던 코드가 오히려 길고 복잡해져요. 패턴은 공짜가 아니에요. 클래스가 늘고, 읽을 코드가 늘죠.
그래서 순서를 거꾸로 잡는 게 좋아요. "이 패턴을 써볼까?" 가 아니라, "지금 내 코드에 이 불편이 실제로 있나?" 를 먼저 물어요. 생성자가 정말 길고 헷갈리는가? 종류가 자주 늘어나는가? 그 불편이 진짜 있을 때, 그때 패턴을 꺼내면 돼요. 불편이 없는데 패턴부터 꺼내면, 그게 바로 과한 설계예요.
💡 디자인 패턴의 진짜 가치는 "이름을 안다" 가 아니라 "이 불편엔 이런 해법이 있다는 걸 안다" 예요. 그래서 우리는 오늘 이름보다 동기를 먼저 봤어요. 면접이든 실무든, 패턴 이름을 줄줄 외우는 사람보다 "왜 여기에 이걸 썼는지" 를 설명하는 사람이 훨씬 믿음직스러워요.
마무리
오늘은 객체를 만들어내는 네 가지 방법을 배웠어요.
- Singleton — 앱에 딱 하나만 있어야 하는 객체를,
enum으로 가장 안전하게. - Builder — 길고 헷갈리는 생성자를, 이름표 붙여 쌓는 방식으로.
- Factory Method — 무엇을 만들지 한 곳에서 결정해, 종류가 늘어도 흔들리지 않게.
- Abstract Factory — 어울리는 한 벌을 통째로, 일관되게.
그리고 무엇보다, 패턴은 불편이 실제로 있을 때 꺼내는 연장이라는 것도요.
다음 시간(Day 41)엔 한 걸음 더 나아가요. 오늘이 객체를 만드는 법이었다면, 다음은 객체의 행동을 갈아끼우는 법이에요. 예를 들어 알림을 보낼 때, 상황에 따라 이메일로 보냈다가 푸시로 바꿔 끼우고 싶을 수 있죠. 이렇게 행동을 통째로 교체하는 Strategy 패턴부터, 변화를 구독해 알림받는 Observer, 기능을 덧입히는 Decorator 까지 — 코드를 유연하게 만드는 패턴들을 이어서 만나봐요. 수고 많으셨어요!
과제
오늘 배운 생성 패턴을 직접 손으로 만들어볼 시간이에요. 셋 다 인스타그램에서 실제로 일어날 법한 상황이에요. 막히면 본문의 코드를 참고하되, 먼저 "이 불편을 어떤 패턴이 풀어주지?" 를 떠올려보세요.
과제 1 (기초): 앱 설정을 담는 Singleton 만들기
상황 배경: 앱 전체가 함께 쓰는 설정값이 있어요. 예를 들어 "한 게시물에 올릴 수 있는 최대 사진 수", "앱 버전" 같은 것들이요. 이런 설정은 앱에 딱 하나만 있어야 해요. 여러 개면 어디는 10장, 어디는 5장으로 제각각이 되니까요. Step 1 의 IdGenerator 를 참고하세요.
🎯 해결 미션:
AppConfig같은 enum 을 만들고,INSTANCE상수 하나를 두세요.- 필드로
maxPhotosPerPost(최대 사진 수)와appVersion(앱 버전 문자열)을 두고, 각각을 읽는 메서드를 만드세요. - 서로 다른 곳에서
AppConfig.INSTANCE를 두 번 불러와, 둘이 정말 같은 객체인지(==로) 확인해보세요. - 왜 이 설정을
enum으로 만들면 "딱 하나" 가 저절로 보장되는지 한 줄로 적어보세요.
과제 2 (응용): 게시물 업로드 옵션을 Builder 로 만들기
상황 배경: 게시물을 올릴 때 옵션이 꽤 많아요. 캡션은 필수지만, 위치 태그·댓글 허용 여부·좋아요 수 숨김 같은 건 켤 수도 끌 수도 있죠. 이걸 생성자 하나로 받으면 new PostUpload("안녕", "서울", true, false) 처럼 순서가 헷갈려요. Step 3 의 ProfileCard 를 참고하세요.
🎯 해결 미션:
PostUpload클래스를 만들고, 필드를 전부final로 두어 불변으로 만드세요. 필수는caption, 선택은location·commentsEnabled·hideLikeCount정도로요.- 생성자를
private으로 막고, 안에 정적 중첩 클래스Builder를 두세요. 선택 항목에는 기본값을 미리 정해두세요(예:commentsEnabled = true). - 각 빌더 메서드가
return this로 자기 자신을 돌려주게 해서,.location(...).hideLikeCount(...)처럼 이어 쓸 수 있게 하세요. - 캡션만 넣은 업로드와, 모든 옵션을 채운 업로드를 각각 만들어, 선택 항목이 기본값/넣은 값으로 잘 들어갔는지 확인하세요.
과제 3 (심화): 알림 객체를 만드는 Factory + 새 종류 추가
상황 배경: 인스타그램은 여러 종류의 알림을 보내요. 좋아요 알림, 댓글 알림, 팔로우 알림처럼요. 종류에 따라 알맞은 알림 객체를 만들어주는 공장을 두면, 알림을 쓰는 화면은 종류만 알려주면 돼요. Step 4 의 ContentFactory 를 참고하세요.
🎯 해결 미션:
- 알림의 부모가 될 추상 클래스나 인터페이스
Notification을 만들고,message()(알림 문구를 돌려주는 메서드)를 약속으로 두세요. LikeNotification·CommentNotification·FollowNotification세 자식을 만들어, 각자 다른 문구를 돌려주게 하세요.NotificationTypeenum(LIKE·COMMENT·FOLLOW)을 받아 알맞은 알림을 만들어주는NotificationFactory.create(...)를switch로 작성하세요.- 이제 새 종류 "멘션 알림"(
MENTION)을 추가한다고 상상해보세요. 코드에서 딱 몇 군데 를 고치면 되는지 세어보고, 만약 공장 없이 화면마다 직접new했다면 몇 군데를 고쳐야 했을지와 비교해 한 줄로 적어보세요.
생각해볼 주제
혼자 고민해도 좋고, 동료와 토론해도 좋아요. 정답을 외우기보다 "나라면 어떻게 설명할까" 를 떠올리며 읽어보세요.
1. Singleton 은 편한데, 왜 "조심해서 쓰라" 는 말도 많을까?
앱에 딱 하나만 두고 어디서나 INSTANCE 로 꺼내 쓰는 건 분명 편해요. 그런데 경험 많은 개발자들은 "싱글턴을 남용하지 말라" 고도 자주 말해요. 편한데 왜 조심하라는 걸까요?
힌트는 "어디서나 꺼내 쓸 수 있다" 는 그 편리함 자체에 있어요. 어디서나 손이 닿는다는 건, 어떤 객체가 이 싱글턴을 몰래 쓰고 있는지 코드만 봐선 드러나지 않는다는 뜻이기도 해요. 또 앱 전체가 같은 하나를 공유하니, 테스트할 때 그 하나의 상태를 깨끗이 되돌리기도 어려워지죠. "전역에서 공유되는 단 하나" 가 주는 편리함과 위험을 같이 저울에 올려보세요.
2. record 로 충분한 때와, Builder 가 필요한 때는 어떻게 갈릴까?
Day 29 에서 배운 record 도 불변 객체를 아주 간단하게 만들어줬어요. 그런데 오늘은 비슷한 불변 객체를 만드는 데 Builder 라는 더 긴 코드를 썼죠. 그러면 둘은 언제 갈라 써야 할까요?
"필드가 몇 개이고, 그중 선택이 얼마나 되는가" 를 떠올려보세요. 필드가 두세 개이고 대부분 필수라면 record 한 줄이 가장 깔끔해요. 반대로 필드가 많고 선택 항목이 뒤섞여 있어서, 생성자로 받으면 순서가 헷갈리고 빈칸이 생긴다면 그때 Builder 가 제값을 해요. "간단한 데 복잡한 도구를 쓰면 그게 과한 설계" 라는 오늘 Step 6 의 이야기와도 이어보세요.
3. Factory 가 "new 를 감추는" 게 왜 좋은 일일까?
Factory 를 쓰면 콘텐츠를 쓰는 화면이 new ImageContent(...) 를 직접 부르지 않고, ContentFactory.create(...) 만 불러요. 어떻게 보면 한 단계를 더 거치는 건데, 왜 이게 더 좋다고 할까요?
"화면이 무엇을 알아야 하는가" 를 생각해보세요. 직접 new 하면, 그 화면은 ImageContent·VideoContent 같은 구체 클래스 이름을 전부 알아야 해요. 그 클래스 중 하나의 이름이 바뀌거나 새 종류가 생기면 화면도 영향을 받죠. 반면 공장에 맡기면 화면은 "콘텐츠를 하나 받는다" 만 알면 돼요. 구체적인 종류는 공장 뒤에 숨고요. 이렇게 서로가 덜 얽혀 있을수록 한쪽을 고쳐도 다른 쪽이 안 흔들린다는 점을, 오늘 배운 다른 패턴들과도 연결해 정리해보세요.
✅ 예시 답안정답 보기
아래는 과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니니, 내 코드와 흐름을 비교하며 "왜 이렇게 했는지" 를 짚어보세요.
과제 예시답안
과제 1 예시답안 — 앱 설정을 담는 Singleton 만들기
핵심 접근
앱 전체가 같은 값을 써야 하는 설정은 딱 하나만 있어야 해요. 여러 개면 어디는 사진 10장, 어디는 5장으로 제각각이 되니까요. Step 1 의 IdGenerator 처럼 enum 으로 만들면, 자바가 INSTANCE 하나만 만들어줘서 "딱 하나" 가 저절로 보장돼요. 우리가 직접 신경 쓸 게 없죠.
예시 구현
// com/instagram/javabasic/design/creational/solution/day40/AppConfig.java
public enum AppConfig {
INSTANCE;
private final int maxPhotosPerPost = 10; // 한 게시물 최대 사진 수
private final String appVersion = "1.0.0"; // 앱 버전
public int getMaxPhotosPerPost() {
return maxPhotosPerPost;
}
public String getAppVersion() {
return appVersion;
}
}
서로 다른 곳에서 AppConfig.INSTANCE 를 두 번 불러와 == 로 비교하면 true 가 나와요. 둘이 정말 같은 객체라는 뜻이죠.
채점 포인트
enum으로 만들고INSTANCE상수 하나만 두었는가- 설정값(
maxPhotosPerPost·appVersion)을 읽는 메서드를 두었는가 - 두 곳에서 불러온
INSTANCE가==로 같은지 확인했는가 - 필드를
final로 두어 설정이 도중에 바뀌지 않게 했는가
흔한 실수
private생성자에static인스턴스를 두는 옛 방식으로 만들려다, 동시에 부르면 두 개가 생기는 함정을 못 막는 경우.enum은 이걸 자바가 알아서 막아줘서 가장 간단하고 안전해요.- "왜 enum 이면 딱 하나가 보장되나" 의 답 —
enum상수는 자바가 프로그램이 켜질 때 단 한 번만 만들어두고, 이후엔 그 하나를 계속 돌려줘요. 우리가new로 더 만들 방법 자체가 없으니, "딱 하나" 가 문법으로 보장되는 거예요.
과제 2 예시답안 — 게시물 업로드 옵션을 Builder 로 만들기
핵심 접근
캡션은 필수지만 위치·댓글 허용·좋아요 숨김은 선택이라, 생성자 하나로 받으면 new PostUpload("안녕", "서울", true, false) 처럼 순서가 헷갈려요. Step 3 의 ProfileCard 처럼, 필수 값으로 시작하는 입구를 두고 선택 항목은 이름표를 붙여 이어 쌓은 뒤 build() 로 완성해요. 필드를 전부 final 로 두어 한 번 만들면 안 바뀌게(불변) 하고요.
예시 구현
// com/instagram/javabasic/design/creational/solution/day40/PostUpload.java
public static class Builder {
private final String caption; // 필수 — 입구에서 받아요
private String location = ""; // 선택 — 기본: 위치 없음
private boolean commentsEnabled = true; // 선택 — 기본: 댓글 허용
private boolean hideLikeCount = false; // 선택 — 기본: 좋아요 수 보임
private Builder(String caption) {
this.caption = caption;
}
public Builder location(String location) {
this.location = location;
return this; // 자기 자신을 돌려줘 점을 이어 찍게 해요
}
public PostUpload build() {
return new PostUpload(this);
}
}
commentsEnabled·hideLikeCount 의 빌더 메서드도 location 과 똑같아요. 쓸 때는 이렇게요.
PostUpload a = PostUpload.builder("첫 게시물").build(); // 캡션만
PostUpload b = PostUpload.builder("제주 여행")
.location("제주도")
.hideLikeCount(true)
.build(); // 선택 일부
캡션만 넣은 a 는 선택 항목이 기본값(location="", 댓글 허용, 좋아요 보임)으로, b 는 넣은 값으로 채워져요.
채점 포인트
- 필드를 전부
final로 두어 불변으로 만들었는가 - 생성자를
private으로 막고Builder로만 만들게 했는가 - 선택 항목에 기본값을 미리 정해두었는가 (
commentsEnabled = true등) - 각 빌더 메서드가
return this로 이어 쓰기를 가능하게 했는가
흔한 실수
- 선택 항목에 기본값을 안 줘서, 안 채운 항목이
null이나 예상 밖 값으로 남는 경우. 빌더 필드에 기본값을 미리 적어두면 "안 넣으면 이 값" 이 분명해져요. - 빌더 메서드에서
return this를 빠뜨리는 경우. 그러면.location(...).hideLikeCount(...)처럼 이어 쓸 수 없어요. 값을 채운 뒤 꼭 자기 자신을 돌려줘야 체이닝이 돼요. setter를 열어두는 경우. 그러면 완성한 뒤에도 값이 바뀔 수 있어 불변이 깨져요. Builder 의 장점 하나가 "완성되면 못 바꾸는 안전함" 이라는 걸 떠올려요.
과제 3 예시답안 — 알림 객체를 만드는 Factory + 새 종류 추가
핵심 접근
좋아요·댓글·팔로우 알림은 종류만 다를 뿐 "문구를 돌려준다" 는 공통 약속을 가져요. 그래서 부모로 Notification 인터페이스를 두고, 종류마다 자식을 만들어요. 그리고 Step 4 의 ContentFactory 처럼, 종류(enum)를 받아 알맞은 알림을 switch 로 골라 만드는 공장을 둬요. 알림을 보내는 쪽은 종류만 알려주면 되고요.
예시 구현
// com/instagram/javabasic/design/creational/solution/day40/Notification.java
public interface Notification {
String message();
}
// com/instagram/javabasic/design/creational/solution/day40/LikeNotification.java
public class LikeNotification implements Notification {
private final String fromUser;
public LikeNotification(String fromUser) {
this.fromUser = fromUser;
}
@Override
public String message() {
return fromUser + " 님이 회원님의 게시물을 좋아합니다.";
}
}
댓글·팔로우 알림도 같은 모양으로 문구만 달라요. 이제 공장이 종류를 보고 알맞은 알림을 골라 만들어요.
// com/instagram/javabasic/design/creational/solution/day40/NotificationFactory.java
public enum NotificationType {
LIKE, COMMENT, FOLLOW
}
public static Notification create(NotificationType type, String fromUser) {
return switch (type) {
case LIKE -> new LikeNotification(fromUser);
case COMMENT -> new CommentNotification(fromUser);
case FOLLOW -> new FollowNotification(fromUser);
};
}
채점 포인트
- 공통 약속
Notification(인터페이스 또는 추상 클래스)에message()를 두었는가 - 종류마다 자식을 만들어 각자 다른 문구를 돌려주게 했는가
NotificationTypeenum 을 받아switch로 알맞은 알림을 만들었는가- 공장이 자식 타입이 아니라 부모 타입
Notification으로 돌려주는가
흔한 실수
- 공장 없이 알림 쓰는 화면마다
new LikeNotification(...)을 직접 부르는 경우. 당장은 되지만, 종류가 늘 때마다 그 화면들을 다 찾아 고쳐야 해요. - 공장이 구체 타입(
LikeNotification)을 돌려주게 만드는 경우. 부모 타입Notification으로 돌려줘야, 받는 쪽이 종류를 몰라도message()한 번으로 처리할 수 있어요(다형성). - "멘션 알림을 추가하면 몇 군데?" 의 답 — 공장을 쓰면 ①
MentionNotification클래스를 새로 만들고 ② enum 에MENTION을 더하고 ③switch에 한 줄을 더하는, 딱 세 군데 면 끝나요. 공장 없이 화면마다 직접new했다면, 알림을 만드는 모든 화면 을 찾아 고쳐야 했겠죠. 이 차이가 Factory 가 풀어주는 불편이에요.
생각해볼 주제 예시답안
생각해볼 주제 1 예시답안 — Singleton 은 편한데, 왜 "조심해서 쓰라" 는 말도 많을까?
[문제 상황 요약]
앱에 딱 하나 두고 어디서나 INSTANCE 로 꺼내 쓰는 건 편한데, 경험 많은 개발자들은 왜 "싱글턴을 남용하지 말라" 고 자주 말하는지에 대한 질문이에요.
[튜터의 가이드 및 해설]
역설적이게도, 문제의 뿌리는 "어디서나 꺼내 쓸 수 있다" 는 그 편리함이에요. 두 가지로 풀어볼게요.
첫째, 숨은 연결을 만들어요. 어떤 클래스가 싱글턴을 INSTANCE 로 슬쩍 꺼내 쓰면, 그 클래스가 무엇에 기대고 있는지 겉으로는 안 보여요. 생성자나 메서드 인자에 안 드러나니까요. 나중에 코드를 읽는 사람은 "이게 저기에 몰래 묶여 있었네?" 하고 뒤늦게 알게 돼요. 연결이 눈에 안 보이면 고치기도 무서워져요.
둘째, 테스트가 어려워져요. 싱글턴은 앱 전체가 같은 하나를 공유해요. 그 하나가 값을 바꿔가며 기억하는(상태를 가진) 객체라면, 한 테스트가 바꿔놓은 상태가 다음 테스트에 그대로 넘어가요. 테스트마다 깨끗한 상태에서 시작하기가 어려워지죠.
그래서 균형이 중요해요. 앱 설정값처럼 한 번 정해지면 안 바뀌고, 정말 하나뿐이어야 자연스러운 것엔 싱글턴이 깔끔해요. 반대로 여기저기서 값을 바꿔 쓰는 객체라면, 싱글턴으로 전역에 두기보다 필요한 곳에 생성자로 건네주는 편이 흐름이 더 잘 보여요.
🎯 면접관을 홀리는 핵심 멘트
"싱글턴의 편리함과 위험은 같은 뿌리예요. '어디서나 꺼내 쓸 수 있다' 는 게 곧 '어디서 쓰는지 코드에 안 드러난다' 는 뜻이고, 전역에서 공유되니 가변 상태일 때 테스트를 격리하기도 어려워져요. 그래서 저는 설정값처럼 불변이고 하나뿐인 게 자연스러운 것에만 싱글턴을 쓰고, 상태를 바꿔 쓰는 객체는 필요한 곳에 건네주는 방식을 선호합니다."
생각해볼 주제 2 예시답안 — record 로 충분한 때와, Builder 가 필요한 때는 어떻게 갈릴까?
[문제 상황 요약]
record 도 불변 객체를 아주 간단히 만들어주는데, 오늘은 비슷한 불변 객체에 더 긴 Builder 코드를 썼어요. 둘을 언제 갈라 써야 하는지에 대한 질문이에요.
[튜터의 가이드 및 해설]
기준은 "필드가 몇 개이고, 그중 선택이 얼마나 되는가" 예요.
필드가 두세 개이고 대부분 필수라면 record 한 줄이 가장 깔끔해요. record Point(int x, int y) 처럼요. 만들 때 값 두 개를 순서대로 넣어도 헷갈릴 일이 없고, 코드도 짧죠. 이럴 때 Builder 를 쓰면 오히려 짧던 코드만 길어져요 — Step 6 에서 말한 "단순한 데 복잡한 도구를 쓰는 과한 설계" 예요.
반대로 필드가 많고 선택 항목이 뒤섞여 있을 때 Builder 가 제값을 해요. 오늘의 PostUpload 처럼 캡션은 필수인데 위치·댓글 허용·좋아요 숨김은 선택이라면, 생성자로 받을 경우 new PostUpload("안녕", "", true, false) 처럼 빈칸과 순서가 헷갈려요. 이럴 때 이름표를 붙여 원하는 것만 골라 채우는 Builder 가 빛나요.
한 문장으로 정리하면, "순서대로 몇 개만 넣으면 끝" 이면 record, "골라 넣을 게 많아 헷갈리면" Builder 예요.
🎯 면접관을 홀리는 핵심 멘트
"필드가 적고 대부분 필수면 record 가 가장 간결하고, 거기에 Builder 를 쓰면 오히려 과한 설계예요. 반대로 필드가 많고 선택 항목이 뒤섞여 생성자 인자의 순서와 빈칸이 헷갈려질 때 Builder 가 제값을 합니다. 저는 '순서대로 몇 개면 끝이냐, 골라 넣을 게 많으냐' 를 기준으로 둘을 가릅니다."
생각해볼 주제 3 예시답안 — Factory 가 "new 를 감추는" 게 왜 좋은 일일까?
[문제 상황 요약]
Factory 를 쓰면 화면이 new ImageContent(...) 를 직접 부르지 않고 ContentFactory.create(...) 만 불러요. 한 단계를 더 거치는 건데 왜 이게 더 좋은지에 대한 질문이에요.
[튜터의 가이드 및 해설]
핵심은 "화면이 무엇까지 알아야 하는가" 예요.
직접 new 하면, 그 화면은 ImageContent·VideoContent·TextContent 같은 구체적인 클래스 이름을 전부 알아야 해요. 그런데 그중 한 클래스의 이름이 바뀌거나, 생성자에 인자가 하나 더 늘거나, 새 종류가 생기면 — 그걸 직접 new 하던 모든 화면이 같이 흔들려요. 서로 단단히 묶여 있는 거죠.
공장에 맡기면 화면은 "콘텐츠를 하나 받는다(Content)" 까지만 알면 돼요. 어떤 구체 클래스를 어떻게 new 하는지는 공장 뒤에 숨어요. 그래서 구체 클래스 쪽이 바뀌어도 화면은 그대로예요. 고칠 곳이 공장 한 군데로 모이는 거예요. 이렇게 서로가 덜 얽혀 있을수록, 한쪽을 고쳐도 다른 쪽이 안 흔들려요. 오늘 배운 패턴들이 결국 노리는 게 다 이거예요 — 변하는 부분을 한곳에 가두고, 나머지는 그 변화로부터 지키는 것.
🎯 면접관을 홀리는 핵심 멘트
"직접 new 하면 그 코드가 구체 클래스 이름을 전부 알아야 하고, 그 클래스가 바뀌면 같이 흔들려요. Factory 에 맡기면 호출부는 부모 타입만 알면 되고 구체 생성은 공장 뒤에 숨어, 종류가 바뀌거나 늘어도 고칠 곳이 공장 한 군데로 모입니다. 결국 생성 패턴의 핵심은 '변하는 부분을 한곳에 가둬 나머지를 지키는 것' 이라고 봅니다."