Day09: 클래스와 객체 (2) — 데이터에 행동을 붙이기
안녕하세요! 여러분의 Java 가이드, 홍순구 튜터입니다.
Day 9에 오신 걸 환영합니다!
지난 시간엔 흩어져 있던 한 사람의 정보 다섯 조각을 Member 라는 설계도 하나로 묶었죠.
평행 배열 다섯 개를 끌고 다니던 코드가 객체 하나로 단정해지는 걸 직접 봤어요.
그런데 지난 시간 마지막에 제가 이런 찜찜함을 남겨뒀어요.
오늘 만든 Member 를 보면 좀 허전한 데가 있다고요.
Member 는 데이터(필드) 만 들고 있고, 정작 그 데이터로 무언가 하는 일은 전부 바깥(MemberDemo)에 떨어져 있었어요.
추천 점수를 계산하는 일은 분명 "회원의 점수"인데,
그 계산 코드는 Member 클래스 안이 아니라 MemberDemo 라는 다른 곳에 살고 있었죠.
"회원의 점수인데, 회원 스스로 계산하면 더 자연스럽지 않을까?"
"member.calculateRecommendScore() 처럼, 객체가 자기 일을 직접 할 수 있다면?"
오늘은 바로 이 이야기예요. 설계도에 데이터(필드) 만 있던 데서 한 발 더 나아가, 행동(메서드) 을 붙여줍니다. 데이터와 행동이 한 덩어리가 되는 순간, 진짜 객체지향이 시작돼요.
오늘의 주제는 "클래스와 객체 (2) — 데이터에 행동을 붙이기" 입니다.
🎯 학습 목표
- 데이터와 그 데이터를 다루는 행동이 한 클래스에 모여야 자연스러운 이유를 설명할 수 있다
- 클래스 안에 인스턴스 메서드(instance method) 를 정의하고
객체.메서드()로 호출할 수 있다 - 메서드 안에서
this가 "지금 이 객체 자신"을 가리킨다는 걸 생성자와 나란히 이해한다 public/private접근제어자로 필드를 숨기고, getter/setter 통로로만 드나드는 캡슐화(encapsulation) 를 안다- 객체마다 따로인 값과, 클래스에 하나뿐인
static값의 차이를 구분한다 main이 왜static인지 마침내 설명할 수 있다- 데이터와 행동을 한곳에 모아 "객체다운"
Member로 분석기를 다시 짠다
Step 1. "데이터에 행동을 붙이자" — 인스턴스 메서드가 필요한 이유
지난 시간 마지막에 남겨둔 찜찜함을 다시 꺼내볼게요.
우리가 만든 Member 객체는 자기 정보(팔로워, 게시물, 활동 일수 등)를 잘 들고 있었어요.
그런데 그 정보로 추천 점수를 계산하는 일은 Member 안이 아니라 바깥에 있었죠.
지난 시간 코드를 떠올려보면 점수 계산은 이렇게 생겼었어요.
[ MemberDemo (바깥) ]
calculateRecommendScore(Member member) {
member 의 팔로워, 게시물, ... 을 꺼내서 계산
}
[ Member (안) ]
username, followers, posts, ... ← 데이터만 있음
(행동은 없음)
회원 객체는 정작 "자기 점수를 어떻게 매기는지" 를 모르고,
그 계산법은 멀찍이 떨어진 MemberDemo 가 들고 있는 모양이었어요.
마치 사람이 자기 키를 모르고, 옆에 있는 사람이 대신 줄자로 재주는 것처럼 어색하죠.
사람이라면 자기 일은 자기가 한다
실생활로 비유해볼게요. 커피 머신을 생각해보세요. 우리는 커피 머신에게 "물 온도 90도로 데우고, 원두 갈아서, 압력 걸어서 내려줘" 라고 일일이 시키지 않아요. 그냥 버튼 하나 누르면 끝이죠. "커피 내려줘" 라고만 말하면 머신이 알아서 자기 안의 부품으로 처리해요.
객체도 똑같아요. 회원 객체에게 "팔로워 꺼내고, 게시물 꺼내고, 곱하고 더해서 점수 내" 라고 바깥에서 일일이 시키는 대신, "너 점수 얼마야?" 라고만 물으면 객체가 자기 안의 데이터로 알아서 계산해주는 게 훨씬 자연스러워요.
[ Member (안) ]
username, followers, posts, ... ← 데이터
calculateRecommendScore() { ... } ← 행동 (이제 안으로 들어옴!)
grade() { ... }
member.calculateRecommendScore() ← "너 점수 얼마야?" 한 마디면 끝
이렇게 객체 안에 들어와서, 그 객체에게 직접 시킬 수 있는 메서드를 인스턴스 메서드(instance method) 라고 불러요. 인스턴스(instance)는 "객체 하나하나"를 가리키는 말이에요. 지난 시간에 만든 객체 하나하나가 다 인스턴스예요. 그러니까 인스턴스 메서드는 "객체 하나하나가 가진 행동" 이라는 뜻이죠.
왜 필요한지 느낌이 오시나요?
데이터와 그 데이터를 다루는 행동이 한곳에 같이 있어야 코드를 읽기도, 고치기도 편해요.
"회원 점수 계산법이 바뀌었어요" 하면 Member 한 군데만 보면 되니까요.
다음 단계에서 바로 이 인스턴스 메서드를 직접 만들어볼게요.
Step 2. 첫 인스턴스 메서드 만들기 — member.calculateRecommendScore()
말로만 들으면 추상적이니, 바로 코드로 만나볼게요.
지난 시간엔 점수 계산이 MemberDemo 안에 calculateRecommendScore(Member member) 모양으로 있었어요.
회원 객체를 매개변수로 받아서 그 안을 꺼내 썼죠.
오늘은 이걸 Member 클래스 안으로 옮깁니다.
안으로 들어오면 더 이상 회원 객체를 매개변수로 받을 필요가 없어요. 자기가 곧 그 회원이니까요.
// com/instagram/javabasic/domain/member/Member.java
// 추천 점수 — 이제 인자 없이 자기 자신(this)의 필드를 직접 써요 (지난 시간엔 MemberDemo 밖에 있었음)
public int calculateRecommendScore() {
int score = 0;
score = score + this.followers / 100; // 팔로워 100명당 1점
score = score + this.posts / 5; // 게시물 5개당 1점
score = score + this.mutualFriends * 10; // 함께 아는 친구 1명당 10점
score = score + this.daysActive / 30; // 활동 30일당 1점
return score;
}
지난 시간 버전과 비교해볼까요?
[ Day 8: 바깥에 있던 모습 ]
calculateRecommendScore(Member member) ← 회원을 매개변수로 받음
member.followers / 100 ← 받은 회원의 필드를 꺼내 씀
[ Day 9: 안으로 들어온 모습 ]
calculateRecommendScore() ← 매개변수 없음!
this.followers / 100 ← 자기 자신(this)의 필드를 직접 씀
가장 눈에 띄는 변화는 괄호 안이 텅 비었다는 거예요.
지난 시간엔 Member member 를 받아야 했는데, 지금은 받을 게 없어요.
왜냐고요? 이 메서드는 이제 Member 클래스 안에 살고 있어서,
"누구의 점수를 계산하지?" 가 정해져 있거든요. 바로 자기 자신이에요.
그래서 member.followers 대신 this.followers 라고 써요.
this 는 "지금 이 메서드를 부른 바로 그 객체" 를 가리켜요. (this 는 다음 단계에서 더 자세히 봐요.)
같은 객체의 다른 메서드 부르기 — grade()
점수만 계산하면 심심하니, 점수를 보고 등급을 매기는 메서드도 같이 봅시다.
// 등급 — 같은 객체의 calculateRecommendScore() 를 다시 불러서 판정해요
public String grade() {
int score = calculateRecommendScore();
if (score >= 300) {
return "강력 추천";
} else if (score >= 150) {
return "추천";
} else if (score >= 70) {
return "보통";
} else {
return "관심 낮음";
}
}
여기서 재밌는 부분은 첫 줄이에요.
grade() 안에서 calculateRecommendScore() 를 그냥 이름만 적어서 불렀죠?
객체 이름도 this 도 안 붙였어요. 그냥 calculateRecommendScore() 라고만요.
왜 그래도 될까요?
grade() 도 Member 안의 인스턴스 메서드라서, 이미 "어떤 객체 안에서 실행 중"인지가 정해져 있어요.
같은 객체 안의 다른 메서드를 부를 땐 이름만 적으면 자기 자신의 메서드를 부르는 거예요.
사실은 this.calculateRecommendScore() 의 this. 가 생략된 거예요.
호출하는 쪽(MemberDemo)에서는 이렇게 써요.
Member member = members[idx];
int score = member.calculateRecommendScore();
System.out.println("추천 점수 : " + score + "점 → " + member.grade());
member.calculateRecommendScore() — "이 회원아, 네 점수 얼마야?"
member.grade() — "그래서 너 등급은?"
점(.) 앞의 member 가 바로 "누구에게 시키는지"예요.
💡 튜터의 결론
인스턴스 메서드는
객체.메서드()로 부르고, 그 안에서 자기 필드를this.필드로 직접 써요. "회원의 점수는 회원이 스스로 계산한다" — 데이터 옆에 행동이 붙은 순간이에요.
Step 3. this 다시 만나기 — 생성자와 메서드에서의 쓰임
this 라는 단어가 지난 시간에도 나왔고 방금도 나왔어요.
처음 보면 좀 알쏭달쏭한데, 사실 정체는 아주 단순해요.
this = 지금 이 객체 자신. 딱 이것뿐이에요.
지난 시간엔 생성자에서 this 를 처음 맛봤죠. 다시 볼게요.
// 매개변수 생성자 — 다섯 가지 정보를 한 번에 받아 객체를 완성해요.
// this.username 의 this 는 "지금 만들어지는 바로 이 객체" 를 가리켜요.
public Member(String username, int followers, int posts, int mutualFriends, int daysActive) {
this.username = username;
this.followers = followers;
this.posts = posts;
this.mutualFriends = mutualFriends;
this.daysActive = daysActive;
totalMembers++;
}
여기서 this.username = username; 을 보세요.
왼쪽 this.username 은 객체 안의 필드, 오른쪽 username 은 매개변수(괄호로 받은 값) 예요.
이름이 똑같아서 헷갈릴 수 있는데, this. 를 붙이면 "내 안의 필드" 라고 콕 집어주는 거예요.
"지금 만들어지는 이 객체의 username 칸에, 받은 값을 넣어라" 라는 뜻이죠.
메서드에서의 this 도 똑같은 원리
이번엔 방금 만든 인스턴스 메서드를 다시 볼게요.
public int calculateRecommendScore() {
int score = 0;
score = score + this.followers / 100; // 팔로워 100명당 1점
score = score + this.posts / 5; // 게시물 5개당 1점
// ...
}
여기 this.followers 의 this 도 정확히 같은 의미예요.
"지금 이 메서드를 부른 그 객체의 followers" 라는 뜻이죠.
나란히 놓고 보면 원리가 하나로 보여요.
생성자의 this.username → "지금 만들어지는 이 객체" 의 username
메서드의 this.followers → "지금 이 메서드를 부른 이 객체" 의 followers
둘 다 this = "지금 이 객체 자신"
jaehoon_dev 객체에게 calculateRecommendScore() 를 부르면 그 안의 this 는 jaehoon_dev 예요.
minji_cafe 객체에게 부르면 같은 메서드라도 this 는 minji_cafe 가 되죠.
한 메서드를 여러 객체가 나눠 쓰는데, this 덕분에 "누구의 데이터인지" 가 자동으로 갈려요.
🙋 학생 질문 — "튜터님, 메서드 안에서 this 를 꼭 붙여야 하나요? 안 붙여도 되던데요?"
좋은 관찰이에요! 매개변수나 지역 변수와 이름이 겹치지 않으면
this.는 생략해도 자바가 알아서 자기 필드라고 이해해요.score + this.followers와score + followers는 (이름이 겹치지 않으면) 똑같이 동작해요.그런데 생성자처럼 매개변수 이름과 필드 이름이 똑같을 때는 반드시 붙여야 해요.
username = username;이라고만 쓰면 자바는 "받은 값을 받은 값에 다시 넣어라" 로 알아듣고 필드는 안 채워지거든요. 그래서this.username = username;으로 "내 필드에" 라고 콕 집어주는 거예요.헷갈릴 땐 그냥 다 붙여도 괜찮아요. 의미가 더 또렷해지니까요.
Step 4. private 필드와 캡슐화 — 필드를 숨기고 getter/setter 로
이번엔 지난 시간에 슬쩍 넘어간 또 하나의 찜찜함을 풀 차례예요.
지난 시간 Member 의 필드는 누구나 바깥에서 직접 건드릴 수 있었어요.
member.followers = -5000; ← 바깥에서 아무 값이나 막 넣을 수 있음 (위험!)
팔로워 수에 음수 -5000 을 넣는 게 말이 되나요?
현실에 음수 팔로워는 없는데, 코드는 아무 검사 없이 그냥 받아들여요.
이런 식이면 객체 안의 데이터가 언제든 엉망이 될 수 있어요.
필드를 숨기자 — private
그래서 필드 앞에 private 이라는 표시를 붙여요.
private 은 "비공개" 라는 뜻으로, 이 클래스 바깥에서는 못 건드린다 는 빗장이에요.
// 한 사람을 이루는 다섯 가지 정보 — 이제 private 으로 숨겨 직접 접근을 막아요
private String username; // 사용자 이름
private int followers; // 팔로워 수
private int posts; // 게시물 수
private int mutualFriends; // 함께 아는 친구 수
private int daysActive; // 활동 일수
이제 MemberDemo 에서 member.followers = -5000; 이라고 쓰면 컴파일러가 "안 돼요!" 라고 막아줘요.
필드가 객체 안에 꽁꽁 숨겨졌으니까요.
그런데 여기서 의문이 생기죠. "숨기면 좋은데, 그럼 팔로워 수를 화면에 보여주려면 어떻게 꺼내요?"
통로를 열어주자 — getter
직접 못 건드리게 막는 대신, 읽기 전용 통로를 따로 열어줘요. 이걸 getter(게터) 라고 불러요. "값을 가져오는(get) 메서드" 라는 뜻이에요.
// ===== 캡슐화: private 필드를 읽는(getter) 통로 =====
public String getUsername() {
return username;
}
public int getFollowers() {
return followers;
}
getFollowers() 는 그냥 followers 값을 돌려주기만 해요.
바깥에서는 member.followers 대신 member.getFollowers() 로 값을 읽어요.
"직접 칸에 손대지 말고, 정해진 창구로만 꺼내가세요" 라는 거죠.
검사하는 통로 — setter
값을 넣는 통로도 만들 수 있어요. 이걸 setter(세터) 라고 해요. 그냥 넣기만 하면 getter 와 다를 게 없는데, setter 의 진짜 힘은 넣기 전에 검사할 수 있다는 거예요.
// setter — 값을 넣기 전에 검사할 수 있어요. 팔로워는 음수가 될 수 없으니 막아요.
public void setFollowers(int followers) {
if (followers < 0) {
System.out.println("팔로워 수는 음수가 될 수 없어요. 0으로 설정해요.");
this.followers = 0;
return;
}
this.followers = followers;
}
이제 누가 member.setFollowers(-5000) 을 부르면,
setter 안의 if 가 음수를 잡아내서 0으로 바로잡아요.
필드를 직접 열어뒀을 땐 불가능했던 일이에요. 통로를 하나로 좁혔기 때문에 그 통로에서 검문할 수 있는 거죠.
이걸 캡슐화라고 불러요
이렇게 필드는 private 으로 숨기고, getter/setter 라는 정해진 통로로만 드나들게 하는 걸
캡슐화(encapsulation) 라고 해요. 캡슐(알약)처럼 알맹이를 껍데기로 감싼다는 뜻이에요.
[ 바깥 (MemberDemo) ]
│ │
getFollowers() setFollowers() ← 정해진 통로(public)로만 드나듦
│ │
┌──────────▼───▼──────────┐
│ Member 객체 │
│ ┌────────────────────┐ │
│ │ private followers │ │ ← 알맹이는 숨겨져 직접 손 못 댐
│ │ private posts │ │
│ └────────────────────┘ │
└─────────────────────────┘
약병을 떠올려보세요. 알약은 병 안에 들어 있고, 우리는 뚜껑을 열어 정해진 방식으로만 꺼내요. 병을 깨서 알약을 마구 쏟지 않죠. 캡슐화도 같은 마음이에요. "중요한 데이터일수록 아무나 직접 못 건드리게 하고, 검증된 통로로만 다루자."
💡 튜터의 결론
private으로 필드를 숨기고 getter 로 읽고 setter 로 검사하며 넣어요. 이게 캡슐화예요. 핵심은 "직접 못 건드리니 불편하다" 가 아니라 "통로를 좁혀서 데이터를 안전하게 지킨다" 예요.
Step 5. static 메서드 vs 인스턴스 메서드 — 클래스에 속하는 것 vs 객체에 속하는 것
지금까지 만든 메서드들(calculateRecommendScore, getFollowers 등)은 전부 객체마다 따로 동작했어요.
jaehoon_dev.getFollowers() 와 minji_cafe.getFollowers() 는 각자 다른 팔로워 수를 돌려주죠.
객체에 딸린 메서드니까요.
그런데 가끔은 "객체 하나하나" 가 아니라 "전체"에 대한 정보가 필요할 때가 있어요.
예를 들어 "지금까지 회원이 총 몇 명 만들어졌지?" 같은 거요.
이건 특정 회원 한 명의 정보가 아니라, Member 라는 종류 전체에 대한 정보예요.
클래스에 하나뿐인 값 — static 필드
이럴 때 쓰는 게 static 이에요. 지난 시간 코드에 이미 슬쩍 들어 있었어요.
// 지금까지 생성된 전체 회원 수 — 객체마다 따로가 아니라 클래스에 하나뿐인 값(모든 객체가 공유)
static int totalMembers = 0;
static 이 붙은 totalMembers 는 객체마다 따로 생기는 게 아니라,
Member 클래스에 딱 하나만 존재해요. 모든 객체가 이 한 칸을 같이 써요.
그래서 생성자가 불릴 때마다 totalMembers++ 로 이 공용 칸을 1씩 늘리면,
객체가 몇 개 만들어졌는지 전체 개수가 자동으로 쌓여요.
[ 객체마다 따로인 값 (인스턴스 필드) ]
jaehoon_dev → followers: 1240 posts: 42
minji_cafe → followers: 8500 posts: 150
seungwoo → followers: 320 posts: 12
(객체 하나당 자기 칸을 따로 가짐)
[ 클래스에 하나뿐인 값 (static 필드) ]
Member.totalMembers : 3
(객체가 몇 개든 칸은 딱 하나, 모두가 공유)
클래스에 속하는 메서드 — static 메서드
totalMembers 는 private 이 아니지만, 전체 회원 수를 읽는 통로도 만들어둘 수 있어요.
이 통로는 특정 객체의 정보가 아니니까, 객체 없이 부를 수 있는 게 자연스러워요.
그래서 메서드에도 static 을 붙여요.
// static 메서드 — 객체 없이 클래스 이름(Member.getTotalMembers())으로 부를 수 있어요
public static int getTotalMembers() {
return totalMembers;
}
부르는 방법이 인스턴스 메서드와 달라요.
// 인스턴스 메서드 — 객체 이름으로 부름 (객체가 있어야 함)
member.getFollowers();
// static 메서드 — 클래스 이름으로 부름 (객체 없이도 부를 수 있음)
Member.getTotalMembers();
점(.) 앞을 보세요.
인스턴스 메서드는 member 라는 객체 이름이 앞에 와요. "이 회원에게 물어봐."
static 메서드는 Member 라는 클래스 이름이 앞에 와요. "회원이라는 종류 전체에게 물어봐."
그래서 main 이 static 이었군요!
여기서 Day 6부터 미뤄둔 떡밥을 회수할게요.
우리가 매번 쓰는 public static void main(String[] args) 에 static 이 붙어 있었죠?
"왜 붙는지는 나중에" 하고 넘어갔던 그 static 이에요.
이제 답이 나와요.
프로그램이 처음 시작될 때는 아직 아무 객체도 만들어지지 않은 상태예요.
new 로 객체를 찍어내는 코드 자체가 main 안에 있으니까요.
객체가 없는데 main 을 부르려면, main 은 객체 없이 부를 수 있어야 해요.
그래서 main 은 static 인 거예요. 객체에 속하지 않고 클래스에 속하니까요.
프로그램 시작 → 아직 객체 0개 → 그래도 main 은 불려야 함
→ 객체 없이 부를 수 있어야 함
→ 그래서 main 은 static!
오랫동안 "그냥 그렇게 쓰는 것" 이던 static 이 이제 이유가 보이죠?
🙋 학생 질문 — "튜터님, 그럼 언제 static 을 붙이고 언제 안 붙여요?"
기준은 딱 하나예요. "객체마다 달라지는 값/행동이냐?" 를 물어보세요.
팔로워 수는 회원마다 다르죠?
jaehoon_dev는 1240,minji_cafe는 8500. 객체마다 다르니까 인스턴스 필드예요. 그걸 읽는getFollowers()도 객체마다 답이 다르니 인스턴스 메서드고요.반면 "전체 회원 수" 는 어떤 회원에게 물어도 답이 같아요. 회원 종류 전체에 하나뿐인 값이죠. 그래서 static 이에요.
헷갈리면 이렇게 물어보세요. "이 값/일이 회원 한 명한테 딸린 거야, 아니면 회원 전체에 하나뿐인 거야?" 전자면 인스턴스, 후자면 static 이에요.
Step 6. 전체 리팩토링 — "객체다운" Member 로 분석기 다시 짜기
이제 흩어져 있던 조각들을 모았으니, 분석기 전체가 어떻게 달라졌는지 볼 차례예요.
지난 시간 MemberDemo 는 점수 계산을 바깥에서 했고, 필드도 직접 꺼냈어요.
오늘은 점수 계산은 객체에게 맡기고, 값은 getter 로 꺼내요. 호출부가 얼마나 깔끔해지는지 보세요.
먼저 회원 객체들을 만드는 부분이에요. 이건 지난 시간과 똑같아요.
// com/instagram/javabasic/domain/member/MemberDemo.java
public static void main(String[] args) {
// 추천 사용자 6명 — 이제 한 명이 Member 객체 하나예요 (지난 시간 평행 배열과 같은 데이터)
Member[] members = {
new Member("jaehoon_dev", 1240, 42, 8, 120),
new Member("minji_cafe", 8500, 150, 23, 365),
new Member("seungwoo", 320, 12, 2, 30),
new Member("soyeon_art", 4100, 88, 15, 210),
new Member("wooseok99", 15800, 320, 40, 500),
new Member("hayoung_food", 2300, 67, 11, 95)
};
// ...
}
점수와 등급은 이제 객체가 직접 계산해요
추천 목록을 출력하는 부분을 볼게요.
지난 시간엔 calculateRecommendScore(member) 처럼 메서드에 회원을 넘겼는데,
오늘은 member.calculateRecommendScore() 처럼 회원에게 직접 시켜요.
// 종합 메서드 — 점수도 등급도 이제 Member 객체 스스로 계산해요
static void printRecommendations(Member[] members) {
for (int i = 0; i < members.length; i++) {
Member member = members[i];
int score = member.calculateRecommendScore();
String grade = member.grade();
System.out.println("@" + member.getUsername()
+ " (팔로워 " + formatFollowers(member.getFollowers()) + ")"
+ " 점수 " + score + "점"
+ " → " + grade);
}
}
달라진 점을 하나씩 짚어볼게요.
member.calculateRecommendScore()— 점수 계산을 객체에게 맡겨요. 매개변수로 회원을 넘기지 않아요.member.grade()— 등급도 객체가 자기 점수를 보고 직접 판정해요.member.getUsername(),member.getFollowers()— 필드가private이라 직접 못 꺼내고 getter 로 읽어요.
검색 메서드도 getter 로 깔끔하게
이름으로 회원을 찾는 메서드도 볼게요.
필드가 숨겨졌으니 members[i].username 대신 members[i].getUsername() 으로 비교해요.
// 이름으로 사용자를 찾아 인덱스를 돌려줘요. 없으면 -1.
static int searchMemberByName(Member[] members, String target) {
for (int i = 0; i < members.length; i++) {
if (members[i].getUsername().equals(target)) {
return i;
}
}
return -1;
}
마지막에 전체 회원 수 출력
main 끝에서 static 값을 읽어 전체 회원 수를 보여줘요.
특정 회원이 아니라 클래스 전체에 묻는 거라, 클래스 이름으로 불러요.
// static 값 시연 — 객체 없이 클래스 이름으로 전체 회원 수를 읽어요
System.out.println();
System.out.println("지금까지 생성된 전체 회원 수: " + Member.getTotalMembers() + "명");
Member.getTotalMembers() — 점 앞이 객체가 아니라 클래스 Member 죠?
객체 6개를 만들었으니 생성자가 6번 불렸고, totalMembers 가 6까지 쌓였을 거예요.
지난 시간과 비교하면 호출부 분위기가 확 달라졌어요.
[ Day 8: 행동이 바깥에 있던 모습 ]
int score = calculateRecommendScore(member.followers, member.posts, ...);
String grade = decideGrade(score); ← 바깥 메서드에 다 넘김
System.out.println(member.username + ...); ← 필드 직접 접근
[ Day 9: 행동이 객체 안으로 들어온 모습 ]
int score = member.calculateRecommendScore(); ← 객체에게 시킴
String grade = member.grade(); ← 객체에게 시킴
System.out.println(member.getUsername() + ...); ← getter 통로로 읽음
"회원이 자기 일을 직접 한다" — 이 한 문장이 코드 곳곳에 스며든 게 보이시나요?
Step 7. 종합: 객체의 책임 설계 + 마무리
오늘 우리가 한 일을 한 발 물러서서 정리해볼게요. 지난 시간엔 데이터를 한 덩어리로 묶었고, 오늘은 그 덩어리에 행동을 붙였어요.
[ Day 8 까지 ]
Member = 데이터(필드) 묶음
행동은 바깥(MemberDemo)에 흩어져 있음
[ Day 9 부터 ]
Member = 데이터(필드) + 행동(메서드) 한 덩어리
"회원에 관한 일은 회원이 한다"
이렇게 "어떤 데이터를, 누가, 어떻게 다룰지" 를 정하는 걸 객체의 책임을 설계한다고 말해요.
회원 점수 계산은 누구 책임? 회원 객체.
회원 정보를 안전하게 지키는 건 누구 책임? private + 캡슐화로 회원 객체 스스로.
전체 회원 수를 세는 건 누구 책임? 회원 한 명이 아니라 클래스 전체(static).
객체를 다루기 전 한 가지 — 진짜 객체가 있는지
지난 시간 마지막에 "객체 배열의 빈 칸을 건드리면?" 이라는 질문을 던졌었죠.
Member[] members = new Member[6]; 처럼 칸만 만들고 객체를 안 채우면,
각 칸은 아무것도 안 가리키는 null 상태라고요.
오늘은 객체에게 메서드를 시키는 법을 배웠어요. member.calculateRecommendScore() 처럼요.
그런데 만약 member 가 null(아무 객체도 안 가리킴)인데 점(.)을 찍어 메서드를 부르면,
"없는 사람한테 일을 시키는" 꼴이라 프로그램이 멈춰요.
그래서 객체에게 점을 찍기 전엔 항상 "이 변수가 진짜 객체를 가리키고 있나?" 를 신경 써야 해요.
지금은 배열을 만들 때 모든 칸을 new Member(...) 로 채웠으니 안전하지만,
앞으로 객체가 비어 있을 수 있는 상황에선 점 찍기 전에 한 번 확인하는 습관을 들이면 좋아요.
(이 "비어 있음을 안전하게 다루는 법" 은 나중에 더 깊이 배울 거예요. 지금은 "null 에 점 찍으면 위험하다" 만 기억하면 충분해요.)
💡 튜터의 결론
좋은 객체는 자기 데이터를 자기가 다뤄요. 데이터(필드)와 행동(메서드)을 한곳에 모으고, 중요한 데이터는
private으로 지키고, 전체에 하나뿐인 값은static으로 두세요. "이 일은 누구 책임이지?" 를 묻는 게 객체지향 설계의 시작이에요.
마무리
오늘 우리는 지난 시간에 만든 설계도에 행동을 붙였어요.
- 인스턴스 메서드 —
member.calculateRecommendScore()처럼 객체가 자기 일을 직접 하게 했어요. 점수 계산이 바깥에서Member안으로 들어왔죠. this— 생성자에서든 메서드에서든 "지금 이 객체 자신" 을 가리킨다는 하나의 원리를 익혔어요.private+ 캡슐화 — 필드를 숨기고 getter 로 읽고 setter 로 검사하며 넣어, 데이터를 안전하게 지켰어요.static— 객체마다 따로인 값과, 클래스에 하나뿐인 값(전체 회원 수)을 구분했어요.main이 왜static인지도 마침내 풀었죠.- 객체의 책임 설계 — "회원에 관한 일은 회원이 한다" 는 마음으로 분석기를 다시 짰어요.
지난 시간엔 데이터를 묶었고, 오늘은 행동을 붙였어요.
이 둘이 합쳐지면서 Member 는 비로소 "살아 있는 객체" 가 됐어요.
스스로 점수를 계산하고, 자기 데이터를 지키고, 등급을 판정하죠.
다음 시간 예고
그런데 회원이 한 종류만 있을까요? 인스타그램을 떠올려보면, 일반 회원도 있고, 게시물을 지울 수 있는 관리자 회원도 있고, 광고 없이 쓰는 프리미엄 회원도 있어요. 이들은 전부 "회원" 이라는 공통점이 있어요. 이름도 있고, 팔로워도 있고, 점수도 계산하죠. 그러면서도 각자 조금씩 다른 점이 있고요.
이럴 때 Member 를 처음부터 세 번 따로 만들면 똑같은 코드를 세 번 쓰게 돼요. 비효율적이죠.
대신 이미 만든 Member 를 물려받아 다른 점만 덧붙일 수 있다면 어떨까요?
일반 회원의 모든 걸 그대로 받고, 관리자만의 "게시물 삭제" 행동을 더하는 식으로요.
다음 시간엔 이렇게 이미 있는 설계도를 물려받아 특화하는 방법, 바로 상속(inheritance) 을 배웁니다.
extends 키워드로 물려받고, 물려받은 메서드를 자기 식으로 바꾸는 메서드 오버라이딩(overriding),
그리고 부모의 생성자를 부르는 super() 키워드까지 만나볼 거예요.
오늘 만든 Member 가 다음 시간엔 여러 종류의 회원으로 갈라지며 더 풍성해져요. 다음 시간에 만나요!
과제
오늘 배운 인스턴스 메서드, this, private + getter/setter, static 을 손에 익히는 과제 세 개예요.
모두 코드베이스 Member 와 같은 패키지에 연습하면 돼요.
과제 1: [기본] Post 클래스에 행동 붙이기
지난 시간 과제에서 게시물을 표현하는 Post 클래스를 만들었죠 (필드와 생성자만요).
오늘은 그 설계도에 행동(인스턴스 메서드) 을 붙여보는 과제예요.
해야 할 일:
Post 클래스에 "좋아요 1 증가" 행동을 인스턴스 메서드로 추가하세요.
요구사항:
Post클래스에void addLike()인스턴스 메서드를 추가하세요. 호출될 때마다this.likeCount를 1 늘리면 돼요.void addLike()안에서this.likeCount = this.likeCount + 1;처럼 자기 필드를 직접 다뤄보세요.main에서 게시물 하나를 만들고post.addLike()를 세 번 불러본 뒤, 좋아요 수가 3 늘었는지 출력으로 확인하세요.
힌트:
- 매개변수가 필요 없어요. 자기 자신의
likeCount만 다루니까요. - 좋아요 수를 화면에 보여줄 땐 getter(예:
getLikeCount())를 먼저 만들어두면 깔끔해요.private int likeCount;로 숨기고getLikeCount()로 읽는 캡슐화를 같이 연습해보세요.
과제 2: [응용] Member에 새 인스턴스 메서드 + 캡슐화
오늘 Member 가 자기 점수를 직접 계산했죠.
이번엔 Member 에게 새로운 계산 하나를 더 맡기고, 필드 보호도 함께 챙기는 과제예요.
만들어야 할 메서드:
double averagePostsPerDay() — 하루 평균 게시물 수를 계산해 돌려준다. (게시물 수 ÷ 활동 일수)
요구사항:
Member클래스 안에 인스턴스 메서드double averagePostsPerDay()를 추가하세요. 안에서this.posts와this.daysActive를 써서 계산하세요.- 나눗셈을 할 때 소수점이 나오도록
(double) this.posts / this.daysActive처럼 해보세요. (정수끼리 나누면 소수점이 잘려요. Day 2의 형 변환 기억나죠?) main에서 회원 한 명에게 이 메서드를 불러"@이름 의 하루 평균 게시물: 0.35개"형식으로 출력하세요.
힌트:
- 활동 일수가 0이면 0으로 나누는 문제가 생겨요.
if (this.daysActive == 0) return 0;처럼 메서드 맨 위에서 막아두면 안전해요. (Step 4 setter 가 음수를 막던 것과 같은 마음이에요.) - 결과 타입이
int가 아니라double인 점을 눈여겨보세요. 메서드가 소수를 돌려줄 수도 있어요.
과제 3: [심화] static 카운터로 "오늘 가입한 회원 수" 세기
오늘 static totalMembers 가 전체 회원 수를 자동으로 셌죠.
이번엔 static 의 원리를 응용해, 직접 카운터를 하나 더 만들어보는 과제예요.
만들어야 할 것:
회원 중 "강력 추천" 등급을 받은 사람이 몇 명인지 세는 static 카운터.
요구사항:
Member클래스에static int strongRecommendCount = 0;필드를 추가하세요.grade()메서드 안에서, 등급이 "강력 추천" 으로 판정될 때strongRecommendCount++;로 카운터를 1 늘리세요.Member에static int getStrongRecommendCount()static 메서드를 만들어 이 값을 읽을 수 있게 하세요.main에서 모든 회원의grade()를 부른 뒤,Member.getStrongRecommendCount()로 "강력 추천 회원 수" 를 출력하세요.
힌트:
- static 필드는 객체마다 따로가 아니라 클래스에 하나뿐이에요. 그래서 어떤 회원의
grade()가 카운터를 늘려도 같은 칸이 쌓여요. - 읽는 메서드는 특정 회원의 정보가 아니니
static이 어울려요.Member.getStrongRecommendCount()처럼 클래스 이름으로 부르게 되죠.
도전 (선택): 같은 회원의 grade() 를 두 번 부르면 카운터가 두 번 늘어나요.
이게 문제가 될지, 어떻게 하면 "회원 한 명당 한 번만" 세게 만들 수 있을지 생각해보세요.
(정답을 찾기보다 "왜 이런 일이 생기는지" 를 떠올려보는 게 목적이에요.)
생각해볼 주제
오늘 만든 "행동을 가진 객체" 너머의 이야기를 세 가지 던져드릴게요. 정답이 정해진 질문이 아니에요. 직접 코드를 떠올리며 본인의 답을 만들어보세요.
1. "getter/setter 가 그냥 필드 공개와 뭐가 다를까?"
오늘 우리는 필드를 private 으로 숨기고 getFollowers(), setFollowers() 통로를 따로 만들었어요.
그런데 곰곰이 생각하면 의문이 들 수 있어요.
"어차피 getter/setter 로 다 읽고 쓸 수 있는데, 그냥 필드를 public 으로 열어두는 거랑 뭐가 달라요?"
getFollowers() 는 그냥 값을 돌려주기만 해요. 정말 필드를 공개한 거랑 똑같아 보이죠.
하지만 setFollowers() 는 음수를 막는 검사가 들어 있었어요.
필드를 직접 열어뒀다면 그 검사를 끼워 넣을 자리가 아예 없었을 거예요.
"지금 당장은 통로가 단순해도, 나중에 검사나 가공을 끼워 넣을 수 있다" 는 게 어떤 가치인지 떠올려보세요.
2. "static 과 인스턴스의 경계는 어떻게 정할까?"
오늘 totalMembers(전체 회원 수)는 static, followers(팔로워 수)는 인스턴스로 나눴어요.
"전체에 하나뿐이면 static, 객체마다 다르면 인스턴스" 라는 기준을 세웠죠.
그런데 현실은 늘 깔끔하지 않아요. 예를 들어 "추천 점수 계산 가중치(팔로워 100명당 1점)" 는 모든 회원에게 똑같이 적용돼요. 이건 회원마다 다른 값일까요, 전체에 하나뿐인 값일까요? 어떤 값을 static 으로 두고 어떤 값을 인스턴스로 둘지 — 그 경계를 정할 때 무엇을 기준으로 삼아야 할지 고민해보세요.
3. "메서드가 자기 객체 데이터를 직접 다루는 게 왜 중요할까?"
오늘 calculateRecommendScore() 는 매개변수 없이 this.followers 를 직접 썼어요.
지난 시간엔 회원을 매개변수로 받아서 member.followers 를 꺼내 썼고요.
결과는 똑같은데, 코드 위치만 안과 밖으로 달라진 거죠.
겉보기엔 사소한 차이 같지만, 이 차이가 코드 전체에 미치는 영향은 꽤 커요. 점수 계산법이 바뀌었을 때 어디를 고치게 될지, 회원 데이터가 바뀌었을 때 누가 그 변화를 가장 먼저 알아야 할지 — "데이터와 그 데이터를 다루는 행동이 한곳에 있다" 는 게 유지보수에 어떤 의미인지 본인의 말로 정리해보세요.
✅ 예시 답안정답 보기
본인 답안을 먼저 작성한 뒤 비교해보세요. 정답이 하나만 있는 건 아니에요. 여기 답안은 모범 사례 중 하나일 뿐, 본인만의 더 깔끔한 풀이가 있다면 그게 답입니다.
세 과제 모두 오늘 배운 인스턴스 메서드·this·private + getter/setter·static 을 익히는 연습이에요. Post 는 지난 시간 과제에서 만든 post 패키지 클래스에, Member 수정은 member 패키지 클래스에 이어 붙이면 됩니다.
🎯 [과제 1 예시 답안] Post 클래스에 행동 붙이기
지난 시간 과제에서 필드와 생성자만 가진 Post 클래스를 만들었죠. 오늘은 그 설계도에 void addLike() 라는 행동(인스턴스 메서드) 을 붙이고, likeCount 를 private 으로 숨겨 getter 로만 읽게 바꾸는 과제입니다.
핵심 접근
좋아요 수는 게시물 스스로가 가진 데이터예요. 그러니 "좋아요 1 증가" 라는 행동도 게시물 객체 안에 두는 게 자연스럽죠. 매개변수는 필요 없어요. 늘릴 대상이 바로 자기 자신의 this.likeCount 니까요. 여기에 likeCount 를 private 으로 숨기고 getLikeCount() 통로를 함께 만들면, 좋아요 수를 직접 휘젓지 못하게 막으면서 읽기는 깔끔하게 열어둘 수 있어요.
예시 코드
먼저 Post 클래스에 likeCount 를 private 으로 바꾸고, 행동과 getter 를 추가합니다.
// com/instagram/javabasic/domain/post/Post.java
public class Post {
private String content; // 게시물 내용
private String authorName; // 작성자 이름
private int likeCount; // 좋아요 수 (private 으로 숨김)
public Post() {
}
public Post(String content, String authorName, int likeCount) {
this.content = content;
this.authorName = authorName;
this.likeCount = likeCount;
}
// 👇 오늘 추가한 행동 — 호출될 때마다 자기 좋아요 수를 1 늘려요
public void addLike() {
this.likeCount = this.likeCount + 1;
}
// 👇 캡슐화: 숨긴 likeCount 를 읽는 통로
public int getLikeCount() {
return likeCount;
}
}
main 에서 게시물을 하나 만들고 addLike() 를 세 번 부른 뒤, 좋아요 수가 늘었는지 확인합니다.
public static void main(String[] args) {
Post post = new Post("오늘 카페 다녀왔어요", "마이멜로디", 0);
System.out.println("처음 좋아요 수: " + post.getLikeCount());
post.addLike();
post.addLike();
post.addLike();
System.out.println("세 번 누른 뒤 좋아요 수: " + post.getLikeCount());
}
실행 결과:
처음 좋아요 수: 0
세 번 누른 뒤 좋아요 수: 3
addLike() 를 부를 때 괄호 안에 아무것도 안 넣어요. 늘릴 대상이 매개변수가 아니라 자기 자신(this)이니까요. 부른 횟수만큼 likeCount 가 한 칸씩 차오르는 게 보이죠.
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 인스턴스 메서드 추가 | void addLike() 를 Post 안에 매개변수 없이 선언했는가 |
높음 |
| this 로 자기 필드 조작 | 메서드 안에서 this.likeCount = this.likeCount + 1; 로 자기 필드를 직접 다뤘는가 |
높음 |
| private + getter 캡슐화 | private int likeCount; 로 숨기고 getLikeCount() 로 읽게 했는가 |
중간 |
| 호출·확인 | post.addLike() 를 세 번 부르고 출력으로 3 증가를 확인했는가 |
중간 |
흔한 실수
static void addLike()로 만든 경우 → 좋아요는 게시물 한 개 한 개마다 다른 값이라 인스턴스 메서드여야 해요.static을 붙이면 객체 없이Post.addLike()가 되어 "어느 게시물의 좋아요냐" 가 사라져요. 객체별로 다른 데이터를 다루는 행동은static을 빼세요.addLike(int count)처럼 매개변수를 받은 경우 → 늘릴 대상은 자기 자신의likeCount라 밖에서 받을 게 없어요. 매개변수를 두면 "무엇을 1 늘릴지" 가 모호해지죠. 자기 데이터만 다루는 행동은 매개변수가 비어요.likeCount = likeCount + 1;처럼this를 빼도 동작은 함 → 이 경우엔 매개변수가 없어this없이도 같은 필드를 가리켜요. 다만 "지금 이 객체의 필드를 만진다" 는 뜻을 분명히 하려고this.를 붙여두는 습관이 좋아요.
실무 개선 포인트 (심화)
지금 addLike() 는 좋아요를 무한정 늘릴 수 있어요. 그런데 실무에서는 "좋아요 취소" 도 필요하고, 좋아요 수가 음수로 내려가면 안 되죠.
그래서 보통 addLike() / removeLike() 를 짝으로 두고, removeLike() 안에서 if (this.likeCount > 0) 로 0 아래로 내려가지 않게 막아요. Step 4 의 setter 가 음수를 막던 것과 똑같은 마음이에요.
더 나아가면 "같은 사람이 두 번 좋아요를 못 누르게" 같은 규칙도 필요한데, 그건 좋아요를 누른 사람 목록을 들고 있어야 가능해서 다음 단계의 이야기예요. 오늘은 "행동을 객체 안에 둔다" 는 감만 잡으면 충분합니다.
🎯 [과제 2 예시 답안] Member에 하루 평균 게시물 계산 추가
Member 에게 "하루 평균 게시물 수" 를 스스로 계산하는 double averagePostsPerDay() 메서드를 맡기는 과제입니다. 정수 나눗셈의 함정과 0 나눗셈 가드까지 함께 챙겨봐요.
핵심 접근
게시물 수 ÷ 활동 일수는 회원 자기 데이터(this.posts, this.daysActive)만으로 계산할 수 있어요. 그러니 매개변수 없는 인스턴스 메서드가 딱 맞죠. 두 가지를 조심하면 돼요. 하나는 정수끼리 나누면 소수점이 잘리니 (double) 형 변환을 끼우는 것, 다른 하나는 활동 일수가 0이면 0으로 나누는 사고가 나니 메서드 맨 위에서 if 로 막아두는 것이에요.
예시 코드
Member 클래스 안에 메서드를 추가합니다.
// com/instagram/javabasic/domain/member/Member.java
// 하루 평균 게시물 수 — 자기 게시물 수 ÷ 활동 일수
public double averagePostsPerDay() {
// 활동 일수가 0이면 0으로 나누는 사고가 나니 먼저 막아요
if (this.daysActive == 0) {
return 0;
}
// (double) 를 앞에 붙여 소수점이 살아남게 해요 (정수끼리 나누면 잘려요)
return (double) this.posts / this.daysActive;
}
main 에서 회원 한 명에게 불러 출력합니다.
Member member = new Member("seungwoo", 320, 42, 2, 120);
double avg = member.averagePostsPerDay();
System.out.println("@" + member.getUsername()
+ " 의 하루 평균 게시물: " + avg + "개");
실행 결과:
@seungwoo 의 하루 평균 게시물: 0.35개
게시물 42개 ÷ 활동 120일 = 0.35 가 그대로 나와요. 만약 (double) 를 안 붙였다면 42 / 120 이 정수 나눗셈이 되어 0 이 나왔을 거예요. 형 변환 한 글자가 결과를 가르죠.
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 반환 타입 double | 메서드 반환 타입을 double 로 두어 소수를 돌려주게 했는가 |
높음 |
| 형 변환 | (double) this.posts / this.daysActive 로 소수점이 잘리지 않게 했는가 |
높음 |
| 0 나눗셈 가드 | if (this.daysActive == 0) return 0; 로 0으로 나누는 사고를 막았는가 |
높음 |
| this 로 자기 필드 사용 | 매개변수 없이 this.posts·this.daysActive 를 직접 썼는가 |
중간 |
흔한 실수
(double)를 안 붙여 0이 나온 경우 →42 / 120은 정수끼리 나눗셈이라 소수점이 잘려0이 돼요.(double)를 나누기 앞쪽 값에 붙여야 자바가 "소수로 계산하라" 고 알아들어요. Day 2 형 변환을 다시 떠올려보세요.(double)(this.posts / this.daysActive)처럼 괄호를 통째로 감싼 경우 → 이미 정수로 나눠0이 된 뒤에 소수로 바꾸니0.0이 나와요. 형 변환은 나누기 전에, 나뉘는 값 하나에만 붙여야 해요.- 0 가드를 빼먹은 경우 → 활동 일수가 0인 신규 회원이 들어오면 0으로 나눠 프로그램이 멈춰요. 화면에 친절히 알리고 0을 돌려주는 한 줄이 사고를 막아줘요.
실무 개선 포인트 (심화)
지금은 0.35개 처럼 소수점이 길게 나올 수 있어요. 화면에 보여줄 땐 "소수점 둘째 자리까지" 같은 형식이 보기 좋죠.
그런데 여기서 중요한 건, 계산은 double 로 정확히 하고 표시 형식은 출력하는 쪽에서 다듬는다 는 분리예요. averagePostsPerDay() 가 반올림까지 해버리면 다른 곳에서 정밀한 값이 필요할 때 곤란해져요.
"값을 만드는 책임" 과 "값을 보여주는 책임" 을 나눠두는 감각은 앞으로 계속 쓰여요. 오늘은 메서드가 정확한 double 을 돌려주는 것까지만 챙기면 충분합니다.
🎯 [과제 3 예시 답안] static 카운터로 "강력 추천 회원 수" 세기
회원 중 "강력 추천" 등급을 받은 사람이 몇 명인지 세는 static 카운터를 직접 만드는 과제입니다. grade() 안에서 카운터를 늘리고, static 메서드로 읽어와요.
핵심 접근
"강력 추천 회원이 전체에 몇 명" 인지는 어느 한 회원의 정보가 아니라 모든 회원을 가로지르는 값이에요. 그래서 객체마다 따로가 아닌, 클래스에 하나뿐인 static 칸이 딱 맞죠. grade() 가 "강력 추천" 을 판정하는 그 순간 카운터를 한 칸 올리고, 읽는 메서드는 특정 회원과 무관하니 static 으로 둬서 Member.getStrongRecommendCount() 처럼 클래스 이름으로 부르게 합니다.
⚠️ 이 과제는 학생이 직접
Member를 고쳐보는 연습이라, 아래grade()는 코드베이스의 원래grade()와 조금 달라요 (카운터 증가 한 줄이 더 들어갔죠). 본인 코드베이스에서 자유롭게 손봐도 괜찮습니다.
예시 코드
Member 클래스에 static 카운터 필드와 읽는 메서드를 추가하고, grade() 안에 한 줄을 끼웁니다.
// com/instagram/javabasic/domain/member/Member.java
// 👇 강력 추천 등급을 받은 회원 수 — 클래스에 하나뿐인 static 카운터
static int strongRecommendCount = 0;
public String grade() {
int score = calculateRecommendScore();
if (score >= 300) {
strongRecommendCount++; // 👈 강력 추천일 때 카운터 1 증가
return "강력 추천";
} else if (score >= 150) {
return "추천";
} else if (score >= 70) {
return "보통";
} else {
return "관심 낮음";
}
}
// 👇 카운터를 읽는 static 메서드 — 특정 회원과 무관하니 클래스 이름으로 불러요
public static int getStrongRecommendCount() {
return strongRecommendCount;
}
main 에서 모든 회원의 grade() 를 부른 뒤 카운터를 출력합니다.
Member[] members = {
new Member("jaehoon_dev", 1240, 42, 8, 120),
new Member("minji_cafe", 8500, 150, 23, 365),
new Member("wooseok99", 15800, 320, 40, 500)
};
for (int i = 0; i < members.length; i++) {
members[i].grade(); // 등급 판정 — 강력 추천이면 내부에서 카운터 증가
}
System.out.println("강력 추천 회원 수: " + Member.getStrongRecommendCount() + "명");
실행 결과:
강력 추천 회원 수: 2명
세 회원 중 minji_cafe 와 wooseok99 가 300점을 넘어 "강력 추천" 이 되고, 그때마다 같은 static 칸이 한 번씩 쌓여 최종 2가 돼요. 어느 객체의 grade() 가 늘리든 칸은 하나라서 전체 합이 모이죠.
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| static 필드 선언 | static int strongRecommendCount = 0; 를 클래스에 하나만 두었는가 |
높음 |
| grade 안에서 증가 | "강력 추천" 분기에서만 strongRecommendCount++; 를 했는가 |
높음 |
| static 메서드로 읽기 | static int getStrongRecommendCount() 로 클래스 이름으로 읽게 했는가 |
중간 |
| 호출·확인 | 모든 회원 grade() 후 Member.getStrongRecommendCount() 로 출력했는가 |
중간 |
흔한 실수
- 카운터를 인스턴스 필드로 만든 경우 →
static을 빼면 회원마다 자기 카운터를 따로 가져, 어떤 회원도 "전체 강력 추천 수" 를 알 수 없어요. 객체를 가로지르는 합계는static칸 하나에 모여야 해요. - 모든 분기에서 증가시킨 경우 → "추천"·"보통" 분기에서도
++를 하면 강력 추천이 아닌데도 세어져요. 증가는score >= 300분기 안에만 두세요. - 객체로 카운터를 읽으려 한 경우 →
members[0].getStrongRecommendCount()도 동작은 하지만, 특정 회원과 무관한 값이라Member.getStrongRecommendCount()처럼 클래스 이름으로 부르는 게 의도가 분명해요. - (도전) 같은 회원의
grade()를 두 번 부른 경우 →grade()를 부를 때마다 무조건++하니, 한 회원을 두 번 판정하면 카운터가 두 번 늘어 실제 인원보다 많게 세어져요. 아래 심화에서 다룹니다.
실무 개선 포인트 (심화)
도전 과제에서 짚은 "같은 회원을 두 번 세는 문제" 가 사실 핵심이에요. grade() 는 등급을 판정만 하는 메서드인데, 카운터까지 올리니 "부르는 횟수" 가 "회원 수" 로 둔갑하죠. 호출할 때마다 부작용(side effect)이 쌓이는 메서드는 이렇게 예상 밖의 결과를 내요.
실무에서는 보통 두 갈래로 풀어요. 하나는 회원마다 "이미 셌는지" 표시하는 boolean counted 칸을 둬서 한 번만 세게 막는 방법. 다른 하나는 더 깔끔하게, "판정" 과 "집계" 를 분리해서 카운터를 grade() 안이 아니라 바깥에서 한 번만 돌며 세는 방법이에요. 어느 쪽이든 "메서드 하나가 판정도 하고 집계도 하면 책임이 섞여 사고가 난다" 는 교훈이 같아요. 오늘은 이 어색함을 직접 느껴본 것만으로 큰 수확입니다.
🤔 생각해볼 주제 예시답안
1. getter/setter 가 그냥 필드 공개와 뭐가 다를까?
[문제 상황 요약]
필드를 private 으로 숨기고 getFollowers(), setFollowers() 통로를 따로 만들었어요. 그런데 getFollowers() 는 그냥 값을 돌려주기만 하니 필드를 public 으로 연 것과 똑같아 보이죠. "어차피 다 읽고 쓸 수 있는데 왜 굳이 통로를 만들까?" 하는 질문이에요.
[튜터의 가이드 및 해설]
핵심은 "지금 같은 통로라도, 나중에 검사·가공을 끼울 자리가 생긴다" 예요.
setFollowers() 를 떠올려보세요. 그냥 값을 넣기만 하는 게 아니라 음수를 막는 검사가 들어 있었죠. 만약 followers 를 public 으로 열어뒀다면 누구나 member.followers = -100; 처럼 말도 안 되는 값을 직접 꽂을 수 있었을 거예요. 그 검사를 끼워 넣을 통로 자체가 없으니까요. 통로를 만들어두면, 들어오고 나가는 길목에 규칙을 세울 수 있어요.
당장은 getFollowers() 가 값만 돌려주더라도, 통로가 있다는 것 자체가 미래를 위한 여지예요. 예를 들어 나중에 "팔로워 수를 1000 이상이면 1.2K 로 보여주자" 가 되면, 필드를 직접 열어뒀을 땐 그 값을 쓰는 모든 곳을 다 찾아 고쳐야 하지만, getter 안에서 가공하면 한 군데만 바꾸면 돼요. 데이터를 어떻게 드나들게 할지에 대한 결정권을 객체가 쥐고 있는 거죠.
이게 캡슐화의 진짜 가치예요. 단순히 "숨긴다" 가 아니라, 데이터에 닿는 길을 한 곳으로 모아서 그 길목에 언제든 규칙을 끼울 수 있게 만드는 거예요. 처음엔 단순한 통로여도, 그 통로가 있다는 것만으로 코드가 변화에 강해집니다.
🎯 면접관을 홀리는 핵심 멘트
"getter/setter 와 public 필드의 차이는 '지금' 이 아니라 '나중' 에 드러납니다. 값을 드나드는 길을 한 곳으로 모아두면, 음수 검사나 형식 가공 같은 규칙을 나중에 그 길목 한 군데에만 끼우면 됩니다. 캡슐화는 데이터를 숨기는 게 아니라, 데이터를 다루는 결정권을 객체에 쥐여주는 것입니다."
2. static 과 인스턴스의 경계는 어떻게 정할까?
[문제 상황 요약]
totalMembers(전체 회원 수)는 static, followers(팔로워 수)는 인스턴스로 나눴어요. "전체에 하나뿐이면 static, 객체마다 다르면 인스턴스" 라는 기준을 세웠죠. 그런데 "추천 점수 가중치(팔로워 100명당 1점)" 처럼 모든 회원에게 똑같이 적용되는 값은 어느 쪽일지 — 그 애매한 경계를 어떻게 정할지 묻는 질문이에요.
[튜터의 가이드 및 해설]
기준을 한 문장으로 다시 쓰면 "이 값이 회원마다 달라질 수 있는가?" 예요.
followers 는 회원마다 분명히 달라요. 그래서 인스턴스 필드죠. totalMembers 는 "전체에 하나" 인 합계라 어느 회원에게 물어봐도 같은 값이어야 해요. 그래서 static 이고요. 여기까진 깔끔합니다.
이제 "팔로워 100명당 1점" 가중치를 봅시다. 이 100이라는 값은 회원마다 다른가요? jaehoon 도 100명당 1점, minji 도 100명당 1점, 모두 똑같죠.
회원이 바뀐다고 가중치가 달라지지 않아요. 그러니 이건 인스턴스가 아니라 모든 회원이 공유하는 static 값 이 어울려요.
게다가 한 번 정하면 거의 안 바뀌는 값이라, 실무에서는 static final 로 두어 "공유되고, 못 바꾸는 상수" 로 정해둬요. (final 은 "한 번 정하면 못 바꾼다" 는 뜻이에요.)
판단 흐름을 그림으로 정리하면 이래요.
이 값을 회원마다 다르게 가질 필요가 있나?
│
┌────┴────┐
YES NO
│ │
인스턴스 전체 공유 → static
(followers) (가중치 100, 등급 경계 300)
│
나중에 바뀌나?
┌─────┴─────┐
YES NO
│ │
static static final
(totalMembers) (가중치 상수)
애매할 땐 이렇게 자문해보세요. "회원 A 의 이 값과 회원 B 의 이 값이 다를 수 있나?" 다를 수 있으면 인스턴스, 항상 같아야 하면 static. 가중치나 등급 경계처럼 "모두에게 똑같이 적용되는 규칙 값" 은 거의 static(필요하면 final 까지) 쪽이에요.
🎯 면접관을 홀리는 핵심 멘트
"static 과 인스턴스의 경계는 '이 값이 객체마다 달라질 수 있는가' 로 갈립니다. 객체마다 다르면 인스턴스, 모든 객체가 공유하는 합계나 규칙 값이면 static 입니다. 가중치나 등급 경계처럼 모두에게 똑같이 적용되고 잘 안 바뀌는 값은 static final 로 두어 공유와 불변을 함께 보장하는 게 실무 관행입니다."
3. 메서드가 자기 객체 데이터를 직접 다루는 게 왜 중요할까?
[문제 상황 요약]
오늘 calculateRecommendScore() 는 매개변수 없이 this.followers 를 직접 썼어요. 지난 시간엔 회원을 매개변수로 받아 member.followers 를 꺼내 썼고요. 결과는 똑같은데 코드 위치만 객체 안과 밖으로 달라진 거죠. 이 사소해 보이는 차이가 유지보수에 어떤 의미인지 묻는 질문이에요.
[튜터의 가이드 및 해설]
핵심은 "데이터와 그 데이터를 쓰는 행동이 한곳에 모이면, 바꿀 때 한곳만 고치면 된다" 예요.
지난 시간처럼 점수 계산이 Member 밖에 있으면, 데이터(followers, posts…)는 Member 안에 있고 그걸 쓰는 계산은 MemberDemo 안에 떨어져 있어요. 둘이 따로 사는 거죠. 그러다 "팔로워 가중치를 100명당 1점에서 200명당 1점으로 바꾸자" 가 되면, 어디를 고쳐야 할까요? 계산이 흩어져 있으면 점수를 계산하는 모든 곳을 다 찾아다녀야 해요. 빠뜨린 한 군데가 버그가 되고요.
반대로 오늘처럼 Member.calculateRecommendScore() 안에 계산이 들어 있으면, 공식이 바뀌어도 Member 클래스 그 메서드 한 곳만 고치면 끝나요. 게다가 데이터가 바로 옆에 있으니, followers 라는 필드가 바뀌면 그걸 가장 먼저 알아야 할 계산 메서드도 같은 클래스 안에서 함께 보여요. 데이터와 행동이 서로를 등지고 있지 않은 거예요.
이걸 응집도(cohesion)라고 불러요. "서로 관련된 것끼리 한곳에 모여 있는 정도" 예요. 응집도가 높으면 "회원에 관한 건 회원 클래스를 보면 다 있다" 가 되어, 코드를 읽는 사람도 고치는 사람도 헤맬 일이 줄어요. 변경 지점이 한곳으로 모이는 것 — 이게 객체 안에 행동을 두는 가장 큰 실무 이득이에요.
비유하면, 자동차의 "달리기" 기능이 차 안 엔진과 함께 있어야 하지, 차 밖 어딘가 따로 떨어져 있으면 엔진을 바꿀 때마다 달리기 코드를 멀리서 찾아 맞춰야 하는 셈이에요. 데이터 옆에 행동을 두면, 둘은 함께 묶여 같이 진화합니다.
🎯 면접관을 홀리는 핵심 멘트
"데이터와 그 데이터를 다루는 행동을 같은 클래스에 두면 응집도가 높아집니다. 점수 공식이 바뀌어도 객체 안 메서드 한 곳만 고치면 되고, 필드가 바뀌어도 그걸 쓰는 행동이 바로 옆에 있어 함께 보입니다. 변경 지점이 한곳으로 모인다는 것 — 이게 행동을 객체 안에 두는 가장 큰 유지보수 이득입니다."