Day08: 클래스와 객체 (1) — 흩어진 정보를 하나로 묶기
안녕하세요! 여러분의 Java 가이드, 홍순구 튜터입니다.
Day 8에 오신 걸 환영합니다! 지난 시간엔 지금까지 배운 도구를 전부 꺼내 인스타 사용자 관리 미니 분석기를 만들었죠. 변수, 배열, 조건문, 반복문, 메서드 — 한 통 가득한 레고 블록으로 작동하는 프로그램 하나를 직접 조립했어요.
그런데 그 프로그램을 만들면서 계속 따라다닌 답답함이 있었어요. 지난 시간 마지막에 제가 "이 답답함, 잘 기억해 두세요" 하고 말씀드렸던 거 기억나시나요?
한 사용자의 정보가 usernames, followers, posts, mutualFriends, daysActive 다섯 배열에 쪼개져 있었어요.
그래서 메서드를 부를 때마다 다섯 개를 줄줄이 같이 넘겨야 했고,
추천 점수를 계산하는 calculateRecommendScore는 매개변수가 네 개나 됐죠.
"한 사람에 대한 정보인데 왜 이렇게 따로 놀지?" 하는 찜찜함이요.
오늘은 그 답답함을 시원하게 풀어드릴 거예요. 흩어진 다섯 조각을 하나의 덩어리로 묶는 방법, 바로 클래스(class) 와 객체(object) 입니다. 프로그래밍을 처음 배우는 분들이 "객체지향"이라는 단어를 들으면 잔뜩 긴장하시는데, 오늘 우리는 거창한 이론이 아니라 지난 시간의 답답함을 푸는 도구로 클래스를 만나볼 거예요.
그리고 오늘은 우리 코드에 작은 전환점이 하나 있어요.
지금까지는 day01, day02 같은 폴더에 간단한 형태로 코드를 담아왔는데,
오늘부터는 정식 클래스라는 정돈된 모양으로 코드를 만들기 시작합니다.
걱정 마세요. 새로운 폴더 정리법 하나가 늘어나는 정도예요. 천천히 같이 가봅시다.
오늘의 주제는 "클래스와 객체 (1) — 설계도와 실체" 입니다.
🎯 학습 목표
- 평행 배열 여러 개로 흩어진 정보를 하나로 묶어야 하는 이유를 설명할 수 있다
- 클래스 = 설계도, 객체 = 설계도로 찍어낸 실체 라는 관계를 이해한다
- 클래스에 필드(field, 인스턴스 변수) 를 정의해 데이터를 담을 수 있다
new키워드로 객체를 만들고, 참조 변수가 객체를 가리키는 원리를 안다- 기본 생성자와 매개변수 생성자의 차이를 알고 직접 만들 수 있다
- 객체가 만들어질 때 메모리(Stack과 Heap)에서 무슨 일이 일어나는지 그림으로 그릴 수 있다
- 지난 시간의 평행 배열 분석기를 객체 배열 버전으로 다시 만들어, 코드가 얼마나 깔끔해지는지 체감한다
Step 1: "평행 배열 다섯 개, 정말 괜찮았을까?" — 클래스가 필요한 이유
본격적으로 새 문법을 배우기 전에, 먼저 왜 이게 필요한지부터 느껴봅시다. 새 도구는 항상 "불편함"에서 출발해요. 불편함을 모르면 도구가 왜 고마운지도 모르거든요.
이번 Step에서는 코드를 거의 쓰지 않아요. 지난 시간 우리가 겪은 답답함을 그림으로 다시 들여다보고, "이걸 어떻게 풀까?"만 함께 고민합니다.
지난 시간의 데이터를 다시 펼쳐봅시다
우리가 다뤘던 추천 사용자 데이터예요. 한 사람의 정보가 다섯 개의 배열에 나뉘어 담겨 있었죠.
[ 평행 배열 — 한 사람이 다섯 군데에 흩어져 있어요 ]
index: 0 1 2
usernames: [ "jaehoon" ] [ "minji" ] [ "seungwoo" ]
followers: [ 1240 ] [ 8500 ] [ 320 ]
posts: [ 42 ] [ 150 ] [ 12 ]
mutualFr.: [ 8 ] [ 23 ] [ 2 ]
daysActive: [ 120 ] [ 365 ] [ 30 ]
↑
"jaehoon" 한 사람의 정보가
다섯 배열의 index 0 자리에 쪼개져 담겨 있음
"jaehoon"이라는 한 사람을 표현하려면 다섯 배열의 0번 인덱스를 동시에 봐야 해요.
usernames[0], followers[0], posts[0] … 이렇게요.
다섯 배열이 "같은 인덱스 = 같은 사람"이라는 약속 위에서만 겨우 맞아떨어지는 구조예요.
이 구조가 왜 위험할까요?
지난 시간에 던진 질문을 다시 떠올려봅시다. 이 평행 배열 방식엔 세 가지 문제가 숨어 있어요.
첫째, 정보 하나만 추가해도 사방을 다 고쳐야 해요.
"프로필이 공개인지 비공개인지"를 추가하고 싶다고 해봐요.
새 배열 isPublic을 하나 만들고, 그 배열을 받는 메서드들의 매개변수를 전부 늘리고,
메서드를 부르는 자리도 싹 손봐야 하죠. 정보 하나 늘렸을 뿐인데 코드 곳곳이 출렁여요.
둘째, 매개변수가 끝없이 늘어나요.
지난 시간 calculateRecommendScore는 이미 매개변수가 네 개였어요.
// 지난 시간 — 한 사람의 정보를 네 조각으로 따로 받았어요
static int calculateRecommendScore(int followers, int posts, int mutualFriends, int daysActive) {
...
}
여기에 정보가 하나 더 늘면 매개변수는 다섯 개, 여섯 개로 계속 불어나요.
호출하는 쪽도 calculateRecommendScore(followers[i], posts[i], mutualFriends[i], daysActive[i]) 처럼 길어지고요.
셋째, 인덱스가 어긋나도 컴파일러가 못 잡아요.
이게 가장 무서운 문제예요.
만약 어느 한 배열만 정렬해서 순서가 살짝 틀어지면 어떻게 될까요?
usernames[2]는 "seungwoo"인데 followers[2]는 엉뚱하게 다른 사람의 팔로워 수를 가리킬 수 있어요.
프로그램은 에러 없이 멀쩡히 돌아가는데 결과만 조용히 틀려요. 이런 버그가 제일 찾기 어렵습니다.
그래서, 흩어진 다섯 개를 하나로 묶는다면?
만약 한 사람의 정보 다섯 개를 하나의 덩어리로 묶어둘 수 있다면 어떨까요?
[ 묶어서 다룬다면 — 한 사람이 한 덩어리예요 ]
members[0] members[1]
┌────────────────────────┐ ┌────────────────────────┐
│ username = jaehoon │ │ username = minji │
│ followers = 1240 │ │ followers = 8500 │
│ posts = 42 │ │ posts = 150 │
│ mutualFriends = 8 │ │ mutualFriends = 23 │
│ daysActive = 120 │ │ daysActive = 365 │
└────────────────────────┘ └────────────────────────┘
한 사람 = 한 덩어리 순서가 어긋날 일이 없음
이러면 세 가지 문제가 한 번에 풀려요. 정보를 추가할 땐 덩어리에 칸 하나만 더하면 되고, 메서드엔 덩어리 하나만 넘기면 되고, 한 사람의 정보가 한 덩어리에 묶여 있으니 인덱스가 어긋날 일도 없죠.
바로 이 "덩어리로 묶는 설계도"가 오늘 배울 클래스(class) 예요. 다음 Step에서 이 설계도를 직접 그려봅시다.
Step 2: "설계도를 그려봅시다" — 첫 번째 클래스 정의
자, 이제 흩어진 다섯 개를 묶을 설계도를 만들어볼 거예요. 그 전에 "클래스"와 "객체"가 정확히 무슨 사이인지 비유로 먼저 잡고 갑시다.
붕어빵 틀과 붕어빵
클래스와 객체의 관계는 붕어빵 틀과 붕어빵이에요.
클래스 (설계도) 객체 (실체)
= 붕어빵 틀 = 찍어낸 붕어빵
┌─────────┐ 🐟 🐟 🐟
│ 틀 1개 │ ──찍어내면──▶ 팥 슈크림 팥
└─────────┘ (각각 다른 붕어빵)
- 틀(클래스) 은 "붕어빵은 이렇게 생겼다"는 설계도예요. 틀 자체는 먹는 빵이 아니죠.
- 붕어빵(객체) 은 그 틀로 찍어낸 진짜 실체예요. 틀 하나로 붕어빵을 여러 개 찍을 수 있고, 안에 든 게 팥이냐 슈크림이냐는 빵마다 달라요.
우리 인스타 예제로 옮기면 이래요.
Member클래스 = "회원 한 명은 이름·팔로워·게시물·함께 아는 친구·활동일수를 가진다"는 설계도Member객체 = "jaehoon", "minji" 처럼 그 설계도로 찍어낸 실제 회원 한 명 한 명
💡 잠깐, 왜 이름이
Member인가요? 지난 시간엔 "사용자"라고 불렀는데, 인스타그램 같은 서비스에서는 가입한 사람을 보통 회원(Member) 이라고 불러요. 그래서 우리 설계도의 이름도Member로 짓겠습니다. 앞으로 이 이름을 쭉 함께 쓸 거예요.
클래스를 코드로 그리기 — 필드부터
설계도에는 "이 물건은 어떤 정보를 담는다"를 먼저 적어요.
Member라는 설계도에 회원 한 명이 가질 정보 다섯 가지를 적어봅시다.
// com/instagram/javabasic/domain/member/Member.java
public class Member {
// 한 사람을 이루는 다섯 가지 정보 — 지난 시간 평행 배열 다섯 개에 대응돼요
String username; // 사용자 이름
int followers; // 팔로워 수
int posts; // 게시물 수
int mutualFriends; // 함께 아는 친구 수
int daysActive; // 활동 일수
}
한 줄씩 뜯어볼게요.
public class Member {— "Member라는 이름의 설계도를 만들겠다"는 선언이에요.class가 바로 설계도를 그리는 키워드예요.- 그 안의
String username;,int followers;… 이 다섯 줄이 설계도에 적힌 칸이에요. - 이렇게 클래스 안에 적어둔 변수를 필드(field) 라고 불러요. "인스턴스 변수"라고도 하는데, 일단 "객체가 가지는 정보 칸" 정도로 기억해두면 충분해요.
여기서 지난 시간과 비교해보면 묶임이 한눈에 보여요.
평행 배열 (지난 시간) Member 클래스 (오늘)
──────────────────── ────────────────────
String[] usernames ──▶ String username;
int[] followers ──▶ int followers;
int[] posts ──▶ int posts;
int[] mutualFriends ──▶ int mutualFriends;
int[] daysActive ──▶ int daysActive;
다섯 개의 따로 노는 배열 하나의 설계도 안에 다섯 칸
다섯 개로 흩어져 있던 배열이, 이제 하나의 설계도 안 다섯 칸으로 들어왔어요. 이게 클래스가 주는 첫 번째 선물이에요. "관련 있는 정보를 한 군데에 모은다."
아직은 설계도일 뿐이에요
한 가지 중요한 점.
이 Member 클래스는 아직 회원 한 명도 만들지 않았어요.
붕어빵 틀만 만들었지, 붕어빵은 아직 안 구운 상태예요.
String username;이라고 적었다고 해서 어딘가에 "jaehoon"이 들어 있는 게 아니에요.
그냥 "회원이라면 이름 칸을 가진다"는 약속만 그려둔 거죠.
그럼 이 설계도로 진짜 회원 객체를 어떻게 찍어낼까요?
바로 다음 Step에서 new라는 키워드로 첫 객체를 구워볼게요.
Step 3: "설계도로 실체를 만든다" — new 키워드와 객체 생성
설계도(Member 클래스)를 그렸으니, 이제 그 설계도로 진짜 회원 한 명을 찍어낼 차례예요.
붕어빵 틀에 반죽을 부어 붕어빵 하나를 굽는 단계입니다.
new — 설계도로 실체를 찍어내는 키워드
객체를 만드는 키워드는 new 예요. 이렇게 씁니다.
// com/instagram/javabasic/domain/member/MemberDemo.java
Member member = new Member();
이 한 줄에 세 가지 일이 한꺼번에 일어나요. 천천히 나눠볼게요.
new Member()—Member설계도로 실제 객체 하나를 새로 찍어내요. 메모리 어딘가에 회원 한 명 분량의 공간이 생겨요.Member member— 그 객체를 가리킬 참조 변수를 하나 만들어요. 타입은Member예요.=— 방금 찍어낸 객체를member변수에 연결해요.
여기서 꼭 짚을 게 하나 있어요.
member라는 변수 안에 객체가 통째로 들어 있는 게 아니에요.
member는 객체가 있는 위치(주소) 만 들고 있어요. 실제 객체는 따로 떨어진 곳에 있고요.
이걸 그림으로 보면 확실해져요.
메모리에서 일어나는 일 — Stack과 Heap
자바는 메모리를 크게 두 구역으로 나눠 써요. Stack과 Heap이에요.
Stack (지역 변수) Heap (실제 객체)
┌──────────────────────┐ ┌──────────────────────────┐
│ member ●───────────┼───────▶│ Member 객체 │
└──────────────────────┘ │ username = null │
│ followers = 0 │
member 변수는 │ posts = 0 │
"객체의 주소" 만 담아요 │ mutualFriends = 0 │
실제 알맹이는 Heap 에 있어요 │ daysActive = 0 │
└──────────────────────────┘
- Stack 에는
member같은 지역 변수가 놓여요. 여기엔 객체의 주소(화살표)만 담겨요. - Heap 에는
new로 찍어낸 진짜 객체가 놓여요. 다섯 칸짜리 알맹이가 여기 있죠. member변수의 화살표가 Heap의 객체를 가리키고 있어요. 그래서member를 "참조 변수"라고 불러요. (참조 = 가리킴)
이 그림이 오늘 가장 중요한 그림이에요. 지난 시간 디버거로 변수 값을 들여다본 적 있죠? Step 7에서 이 Stack과 Heap을 디버거로 직접 눈으로 볼 거예요.
기본값 — 아직 아무것도 안 채웠을 때
위 그림에서 객체의 칸들이 null과 0으로 채워진 게 보이시나요?
new Member()로 갓 찍어낸 객체는 아직 아무 정보도 안 넣었어요.
그래도 칸은 비어 있지 않고 각 타입의 기본값으로 채워져요.
String username→null(문자열의 기본값. "아직 아무 문자열도 안 가리킨다"는 뜻)int followers,posts,mutualFriends,daysActive→0(정수의 기본값)
객체의 칸에 값을 넣고 꺼내기 — 점(.) 연산자
이제 갓 찍어낸 빈 객체의 칸을 채워봅시다.
객체의 칸에 접근할 땐 점(.) 을 써요. "이 객체의, 이 칸" 이라고 콕 짚는 거예요.
Member member = new Member(); // 빈 회원 객체 하나 (모든 칸이 기본값)
member.username = "jaehoon"; // username 칸에 값을 넣어요
member.followers = 1240; // followers 칸에 값을 넣어요
System.out.println(member.username); // 꺼낼 때도 점(.) — jaehoon 출력
System.out.println(member.followers); // 1240 출력
member.username은 "member 객체의 username 칸"이라는 뜻이에요.
값을 넣을 때도, 꺼낼 때도 똑같이 점을 써요.
값을 넣고 나면 아까 그림의 칸들이 이렇게 채워져요.
Stack Heap
┌──────────────┐ ┌──────────────────────────┐
│ member ●─────┼───────▶│ username = "jaehoon" │
└──────────────┘ │ followers = 1240 │
│ posts = 0 │
│ mutualFriends = 0 │
│ daysActive = 0 │
└──────────────────────────┘
username과 followers 칸이 채워졌죠? 나머지는 아직 기본값 0 그대로고요.
이렇게 칸을 하나씩 채워도 되지만, 회원 한 명 만들 때마다 다섯 줄씩 member.xxx = ...을 쓰려니 좀 번거롭죠?
"객체를 만들면서 동시에 다섯 값을 한 번에 넣을 수 없을까?" 하는 생각이 들 거예요.
바로 그걸 해주는 게 다음 Step의 생성자(constructor) 예요.
Step 4: "정보를 담으면서 만들기" — 생성자
지난 Step에서 객체를 만들고 칸을 채우려니 줄이 길어졌어요.
Member member = new Member();
member.username = "jaehoon";
member.followers = 1240;
member.posts = 42;
member.mutualFriends = 8;
member.daysActive = 120;
회원 한 명 만드는 데 여섯 줄이라니, 여섯 명이면 서른여섯 줄이에요. 객체를 찍어내는 그 순간에 값까지 한 번에 채울 수 있다면 훨씬 깔끔하겠죠? 그 역할을 하는 게 생성자(constructor) 예요.
생성자란? — 객체가 태어날 때 딱 한 번 불리는 특별한 코드
생성자는 "객체가 new로 만들어지는 바로 그 순간 자동으로 실행되는" 특별한 코드예요.
사실 우리는 이미 생성자를 쓰고 있었어요. new Member() 의 그 괄호 () 가 바로 생성자를 부르는 자리거든요.
생성자에는 두 종류가 있어요. 하나씩 봅시다.
1. 기본 생성자 — 아무 값도 안 받는 생성자
먼저, 우리가 Step 3에서 썼던 new Member() 가 부르던 생성자예요.
// com/instagram/javabasic/domain/member/Member.java
public Member() {
}
- 이름이 클래스 이름과 똑같아요 (
Member). 생성자의 첫 번째 규칙이에요. - 반환 타입이 없어요.
void조차 안 붙여요. 이것도 생성자만의 특징이에요. - 괄호 안이 비어 있고(매개변수 없음), 본문도 비어 있어요. 그래서 아무 값도 안 채우고 빈 객체만 만들죠.
이렇게 매개변수가 없는 생성자를 기본 생성자라고 불러요.
new Member() 의 빈 괄호가 바로 이 기본 생성자를 부르는 거였어요.
2. 매개변수 생성자 — 값을 받아서 채우는 생성자
이제 우리가 진짜 원하던 것, "만들면서 동시에 값 채우기"를 해주는 생성자예요.
// com/instagram/javabasic/domain/member/Member.java
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;
}
괄호 안에 다섯 개의 매개변수가 생겼어요. 객체를 만들 때 이 다섯 값을 받아서, 본문에서 객체의 칸에 하나씩 넣어줘요.
this — "지금 만들어지는 바로 이 객체"
본문에 this.username = username; 같은 줄이 보이죠? 여기 등장하는 this 가 오늘의 작은 고비예요. 천천히 봅시다.
문제가 하나 있어요. 매개변수 이름도 username이고, 객체의 필드 이름도 username이에요. 똑같죠.
그냥 username = username; 이라고 쓰면 자바는 "둘 다 매개변수 username을 말하는 거네?" 하고 헷갈려요.
그래서 "지금 만들어지고 있는 이 객체의 칸" 을 콕 집어 가리키는 말이 필요해요. 그게 this예요.
this.username = username
↑ ↑
이 객체의 괄호로 받은
username 칸 매개변수 값
(왼쪽) (오른쪽)
this.username— "지금 만드는 이 객체의 username 칸" (왼쪽, 값을 받는 쪽)username— "괄호로 넘어온 매개변수" (오른쪽, 넣을 값)
즉 this.username = username; 은 "넘어온 값을 이 객체의 username 칸에 넣어라"는 뜻이에요.
this는 "나 자신, 이 객체"를 가리키는 거울 같은 거라고 기억해두면 편해요.
생성자를 쓰면 — 한 줄로 끝나요
이제 매개변수 생성자 덕분에, Step 3의 여섯 줄이 한 줄로 줄어요.
// 빈 객체 만들고 → 다섯 줄로 채우기 (X)
// 만들면서 동시에 다섯 값 채우기 (O)
Member member = new Member("jaehoon", 1240, 42, 8, 120);
new Member(...) 괄호 안에 다섯 값을 순서대로 넣으면,
매개변수 생성자가 그 값들을 받아 객체의 다섯 칸을 한 번에 채워줘요.
이 한 줄이 끝나는 순간, 다섯 칸이 전부 채워진 완성된 회원 객체 하나가 만들어져요.
훨씬 깔끔하죠? 이제 이 한 줄짜리 객체 생성으로, 지난 시간의 회원 여섯 명을 배열에 담아볼 거예요.
Step 5: "배열도 객체를 담을 수 있어요" — 객체 배열
지금까진 회원 한 명(member 객체 하나)만 다뤘어요.
그런데 지난 시간 분석기엔 회원이 여섯 명이었죠.
여섯 명을 어떻게 담을까요? 답은 이미 우리 손에 있어요. 바로 배열이에요.
배열에는 객체도 담을 수 있어요
Day 5에서 배운 배열을 떠올려봅시다.
int[]는 정수를 여러 개, String[]은 문자열을 여러 개 담았죠.
같은 방식으로 Member[] 는 Member 객체를 여러 개 담을 수 있어요.
// com/instagram/javabasic/domain/member/MemberDemo.java
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)
};
지난 시간엔 다섯 개의 배열(usernames, followers …)을 따로 만들었는데,
이제 Member[] 배열 하나에 회원 여섯 명이 통째로 들어왔어요.
배열의 각 칸(members[0], members[1] …)에 매개변수 생성자로 찍어낸 객체가 하나씩 담겨요.
객체 배열의 메모리 구조
객체 배열도 그림으로 보면 명확해요. 배열의 각 칸은 객체를 가리키는 주소를 담아요.
Stack Heap
┌───────────┐ ┌──────────────────────────────────────┐
│ members ●─┼───▶│ Member[] 배열 │
└───────────┘ │ [0] ●──▶ Member(username="jaehoon_dev"…)│
│ [1] ●──▶ Member(username="minji_cafe"… )│
│ [2] ●──▶ Member(username="seungwoo"… )│
│ [3] ●──▶ Member(username="soyeon_art"… )│
│ [4] ●──▶ Member(username="wooseok99"… )│
│ [5] ●──▶ Member(username="hayoung_food"…)│
└──────────────────────────────────────┘
members변수는 배열을 가리켜요.- 배열의 각 칸(
[0]~[5])도 값을 통째로 품는 게 아니라, 각Member객체를 가리키는 주소를 담아요. - 그래서 한 사람의 정보 다섯 칸이 객체 하나 안에 묶여 있어요. 인덱스가 어긋날 걱정이 사라졌죠.
객체 배열 순회하기
배열을 도는 방법은 Day 5에서 배운 그대로예요. for 루프로 한 칸씩 돌면 돼요.
다만 각 칸이 Member 객체이니, 안의 정보는 점(.) 으로 꺼내요.
for (int i = 0; i < members.length; i++) {
Member member = members[i]; // i번째 칸의 객체를 꺼내요
System.out.println("@" + member.username + " 팔로워 " + member.followers);
}
members[i]— 배열 i번째 칸의Member객체member.username,member.followers— 그 객체의 칸 값
지난 시간엔 usernames[i], followers[i] 처럼 배열 다섯 개의 같은 인덱스를 동시에 봐야 했어요.
이제는 members[i] 객체 하나만 꺼내면 그 안에 다섯 정보가 다 들어 있어요.
훨씬 자연스럽죠? 이 차이가 다음 Step에서 더 확 와닿을 거예요.
Step 6: "지난 시간 분석기를 객체 배열로 다시 짜기" — 리팩토링
자, 오늘의 클라이맥스예요.
지난 시간에 만든 분석기의 메서드들을, 방금 배운 Member 객체 배열 버전으로 다시 짜볼 거예요.
같은 기능인데 코드가 얼마나 깔끔해지는지 직접 비교해봅시다.
💡 여기서 중요한 약속 하나. 가중치(팔로워 100명당 1점 등)와 등급 경계(300점 이상 강력 추천 등)는 지난 시간과 똑같이 유지해요. 그래야 "같은 데이터 → 같은 결과"가 나오는지 비교할 수 있거든요. 바뀌는 건 코드 모양이지 결과가 아니에요.
가장 극적인 변화 — calculateRecommendScore
지난 시간의 점수 계산 메서드는 매개변수가 네 개였어요.
// 지난 시간 — 한 사람의 정보를 네 조각으로 따로 받았어요
static int calculateRecommendScore(int followers, int posts, int mutualFriends, int daysActive) {
int score = 0;
score = score + followers / 100;
score = score + posts / 5;
score = score + mutualFriends * 10;
score = score + daysActive / 30;
return score;
}
이제 Member 객체 하나만 받으면 돼요.
// com/instagram/javabasic/domain/member/MemberDemo.java
// 추천 점수 계산 — 지난 시간엔 정보 네 개를 따로 받았지만,
// 이제 Member 한 개만 받아서 그 안에서 필요한 값을 꺼내 써요.
static int calculateRecommendScore(Member member) {
int score = 0;
score = score + member.followers / 100; // 팔로워 100명당 1점
score = score + member.posts / 5; // 게시물 5개당 1점
score = score + member.mutualFriends * 10; // 함께 아는 친구는 가중치 큼 (1명당 10점)
score = score + member.daysActive / 30; // 활동 30일당 1점
return score;
}
매개변수가 네 개에서 한 개로 줄었어요. 계산식은 똑같고, 값을 member. 으로 꺼내 쓰는 것만 달라졌죠.
부르는 쪽도 비교해볼까요?
지난 시간 (매개변수 4개 줄줄이):
calculateRecommendScore(followers[i], posts[i], mutualFriends[i], daysActive[i])
오늘 (Member 1개만):
calculateRecommendScore(members[i])
길게 줄지어 넘기던 네 개가 객체 하나로 줄었어요.
바로 이게 오프닝에서 말한 "매개변수가 끝없이 늘어나는 답답함"의 진짜 해결책이에요.
이제 회원 정보에 칸이 몇 개가 더 생겨도, 이 메서드의 괄호는 (Member member) 하나 그대로예요.
전체 목록 출력도 깔끔해져요
목록 출력 메서드도 봅시다.
// com/instagram/javabasic/domain/member/MemberDemo.java
// 전체 사용자를 한 명씩 순회하며 출력 — 객체 배열이라 member.username 으로 바로 꺼내요
static void printAllMembers(Member[] members) {
for (int i = 0; i < members.length; i++) {
Member member = members[i];
System.out.println("@" + member.username
+ " 팔로워 " + formatFollowers(member.followers)
+ " 게시물 " + member.posts + "개");
}
}
지난 시간엔 printAllUsers(String[] usernames, int[] followers, int[] posts) 처럼 배열 세 개를 매개변수로 받았어요.
이제는 Member[] members 하나만 받아요.
안에서 member.username, member.followers, member.posts를 한 객체에서 꺼내 쓰니, "이 팔로워가 이 이름이 맞나?" 헷갈릴 일도 없어요.
결과는 지난 시간과 똑같아요
코드 모양은 확 바뀌었지만, 실행 결과는 지난 시간과 한 글자도 다르지 않아요.
예를 들어 minji_cafe(팔로워 8500, 게시물 150, 함께 아는 친구 23, 활동 365일)의 점수를 계산하면,
85 + 30 + 230 + 12 = 357점 → "강력 추천" 이 그대로 나와요.
jaehoon_dev는 12 + 8 + 80 + 4 = 104점 → "보통" 이고요.
이 동작은 코드베이스의 MemberDemoTest 에서 여러 사례로 검증해 뒀어요.
"리팩토링은 모양을 바꾸되 결과는 지키는 일"이라는 걸 숫자로 확인한 셈이에요.
평행 배열 다섯 개를 끌고 다니던 답답함이, 이제 완전히 사라졌죠? 한 가지만 더 해봅시다. 이 객체들이 메모리에서 진짜로 어떻게 사는지, 디버거로 직접 눈으로 보는 거예요.
Step 7: "메모리를 눈으로 봅시다" — 디버거로 Stack과 Heap 관찰
지금까지 Stack/Heap 그림을 여러 번 그렸어요. 그런데 그림은 어디까지나 그림이죠. 진짜로 그렇게 도는지, 이번엔 디버거로 직접 확인해봅시다. 지난 시간에 디버거로 변수 값을 한 줄씩 따라가 봤으니, 오늘은 그 도구로 객체를 들여다보는 거예요.
중단점을 어디에 찍을까?
MemberDemo의 main에서, 객체 배열을 다 만든 직후 줄에 중단점(Breakpoint) 을 찍어요.
줄 번호 왼쪽 여백을 클릭하면 빨간 점이 생기죠.
Member[] members = {
new Member("jaehoon_dev", 1240, 42, 8, 120),
...
};
// 👈 이 줄(배열 생성 완료 직후)에 중단점을 찍고 디버그 실행
printAllMembers(members);
그리고 "Debug" 버튼(벌레 모양)으로 실행하면, 프로그램이 중단점에서 멈춰요.
디버거에서 무엇이 보이나요?
멈춘 상태에서 왼쪽 아래 Variables 창을 보면, members 변수를 펼쳐볼 수 있어요.
이때 그림으로 그렸던 구조가 실제로 나타나요.
members옆에Member[6]이라고 떠요. "Member 객체 6개짜리 배열"이라는 뜻이에요. (Stack에 놓인 참조 변수)- 삼각형을 펼치면
[0],[1]…[5]칸이 나오고, 각 칸이Member@...같은 표시를 달고 있어요. 이@숫자가 바로 객체의 주소예요. (Heap에 놓인 실제 객체) - 다시
[0]을 펼치면username = "jaehoon_dev",followers = 1240… 다섯 칸이 채워진 게 보여요.
말로만 듣던 "Stack의 참조 변수가 Heap의 객체를 가리킨다"가 화면에 그대로 나타나는 순간이에요.
배열 변수 하나(members)를 펼치면 객체 여섯 개가 주렁주렁 매달려 있고,
객체 하나를 또 펼치면 다섯 칸이 들어 있는 층층이 묶인 구조를 직접 보게 됩니다.
한 줄씩 따라가며 객체가 태어나는 순간 보기
중단점을 배열 생성 줄보다 위에 찍고 한 줄씩(Step Over) 내려가 보면 더 재밌어요.
new Member(...) 가 실행되는 매 순간 Heap에 객체가 하나씩 새로 생기고,
배열의 칸이 그 객체를 하나씩 가리키기 시작하는 모습을 단계별로 볼 수 있어요.
이렇게 디버거로 메모리를 직접 보면, "객체는 Heap에 살고 변수는 그 주소만 들고 있다"는 말이 더 이상 외울 문장이 아니라 눈으로 본 사실이 돼요. 앞으로 객체가 헷갈릴 때마다, 오늘처럼 디버거를 열어 직접 펼쳐보면 됩니다.
마무리
오늘 우리는 지난 시간의 가장 큰 답답함을 풀었어요. 흩어진 정보를 하나로 묶는 방법을 배웠죠.
- 클래스 = 설계도, 객체 = 실체 — 붕어빵 틀 하나로 붕어빵 여러 개를 찍어내듯,
Member클래스 하나로 회원 객체를 여러 개 만들었어요. - 필드(field) — 한 사람의 정보 다섯 개를 클래스 안 다섯 칸에 묶었어요. 평행 배열 다섯 개가 설계도 하나로 들어왔죠.
new와 참조 변수 —new로 Heap에 객체를 찍어내고, 변수는 그 주소만 가리킨다는 걸 Stack/Heap 그림으로 봤어요.- 생성자와
this— 객체가 태어나는 순간 값을 한 번에 채우는 매개변수 생성자, 그리고 "이 객체"를 가리키는this를 익혔어요. - 객체 배열로 리팩토링 — 매개변수 네 개짜리 메서드가
Member하나만 받도록 깔끔해졌고, 결과는 지난 시간과 똑같이 지켜졌어요.
평행 배열 다섯 개를 한 몸처럼 끌고 다니던 코드가, 오늘 Member 객체 하나로 묶이며 한결 단정해졌어요.
"흩어진 정보를 하나의 설계도로 묶는다" — 이게 객체지향의 가장 첫 번째 마음가짐이에요.
다음 시간 예고
그런데 오늘 만든 Member를 가만히 보면 좀 허전한 데가 있어요.
Member는 데이터(필드) 만 가지고 있고, 정작 그 데이터로 무언가 하는 일은 전부 바깥(MemberDemo)에 있죠.
예를 들어 점수를 계산하는 calculateRecommendScore(Member member) 는 Member 객체를 받아 쓰면서도,
정작 Member 클래스 밖에 떨어져 있어요.
"회원의 추천 점수인데, 회원 스스로 계산하면 더 자연스럽지 않을까?"
"member.calculateScore() 처럼, 객체가 자기 일을 직접 할 수 있다면?"
다음 시간(Day 9)엔 바로 이 이야기예요. 데이터에 행동을 붙이는 방법, 즉 인스턴스 메서드를 배웁니다.
그러면서 this의 진짜 쓰임, public과 private으로 칸을 보호하는 캡슐화,
그리고 Day 6부터 슬쩍 미뤄둔 static의 진짜 의미까지 한 번에 풀어드릴게요.
오늘 만든 설계도에 "행동"을 붙이는 순간, 진짜 객체지향이 시작돼요. 다음 시간에 만나요!
과제
오늘 배운 클래스·필드·생성자·객체 배열을 손에 익히는 과제 세 개예요.
모두 코드베이스 MemberDemo 와 같은 패키지에 새 클래스를 만들어 연습하면 돼요.
과제 1: [기본] Member에 정보 한 칸 추가하기
Step 1에서 "평행 배열은 정보 하나만 추가해도 사방을 다 고쳐야 한다"고 했죠. 클래스에선 정말 간단한지 직접 확인해보는 과제예요.
해야 할 일:
Member 클래스에 "비공개 계정 여부" 를 담는 필드 boolean isPrivate 를 하나 추가하세요.
요구사항:
Member클래스에boolean isPrivate;필드를 한 칸 추가하세요.- 매개변수 생성자도 이 값을 받도록 마지막 매개변수로
boolean isPrivate를 추가하고, 본문에this.isPrivate = isPrivate;한 줄을 더하세요. main에서 회원을 만들 때 마지막 인자로 공개/비공개 여부(true/false)를 넘겨보세요.- 출력할 때 비공개 계정이면 이름 뒤에
🔒표시를 붙여 보여주세요.
힌트:
- 평행 배열이었다면
boolean[] isPrivate배열을 새로 만들고, 그 배열을 받는 메서드를 전부 고쳐야 했어요. 클래스에선 칸 하나 + 생성자 한 줄이면 끝나는 걸 비교하며 느껴보세요. - 출력에서 조건 분기는 Day 3에서 배운
if를 쓰면 돼요.if (member.isPrivate) { ... }
과제 2: [응용] 새 설계도 만들기 — Post 클래스
Member 가 "회원"의 설계도였다면, 이번엔 "게시물"의 설계도를 직접 만들어보는 과제예요.
설계도를 처음부터 끝까지 혼자 그려보면 클래스가 손에 익어요.
만들어야 할 것:
게시물 한 개를 표현하는 Post 클래스. 같은 패키지에 Post.java 로 만드세요.
요구사항:
- 필드 세 개:
String content(게시물 내용),String authorName(작성자 이름),int likeCount(좋아요 수) - 기본 생성자와, 세 값을 받는 매개변수 생성자를 모두 만드세요. 생성자 본문에서
this를 써서 칸을 채우세요. - 데모용
main이 있는 클래스에서Post[]배열에 게시물 서너 개를 담고,for루프로 순회하며"[작성자] 내용 (좋아요 N개)"형식으로 출력하세요.
힌트:
Member를 만든 과정을 그대로 따라 하면 돼요. 필드 → 기본 생성자 → 매개변수 생성자 순서요.- 객체 배열 선언은
Post[] posts = { new Post(...), new Post(...) };모양이에요.
과제 3: [심화] 가장 활발한 회원 찾기
Step 6의 findTopFollowers 가 "팔로워가 가장 많은 회원"을 찾았다면,
이번엔 다른 기준으로 한 명을 뽑아보는 과제예요. 객체 배열을 한 바퀴 돌며 "최댓값의 위치"를 찾는 패턴을 연습합니다.
만들어야 할 메서드:
static Member findMostActive(Member[] members) — 활동 일수(daysActive)가 가장 큰 회원 객체를 돌려준다.
요구사항:
Member[]배열을for루프로 돌면서daysActive가 가장 큰 회원을 찾으세요.- 찾은 회원 객체 자체를 반환하세요. (인덱스가 아니라
Member객체를 돌려주는 게 포인트예요. 반환 타입이Member인 걸 눈여겨보세요.) main에서 이 메서드를 불러"가장 활발한 회원: @이름 (활동 N일)"을 출력하세요.
힌트:
- "가장 큰 값의 위치 찾기"는 Day 5·Day 7에서 해본 패턴이에요.
Member maxMember = members[0];으로 시작해, 더 큰 걸 만나면maxMember를 갈아끼우면 돼요. - 메서드가 객체를 반환할 수 있다는 게 새로운 점이에요.
int나String을 반환하던 것처럼,Member객체도 똑같이 반환할 수 있어요.
도전 (선택): 활동 일수가 똑같은 회원이 두 명이면 누가 뽑힐까요? 코드를 보고 "먼저 나온 사람"인지 "나중에 나온 사람"인지 예측해본 뒤, 직접 데이터를 바꿔 확인해보세요.
생각해볼 주제
오늘 만든 Member 설계도 너머의 이야기를 세 가지 던져드릴게요.
정답이 정해진 질문이 아니에요. 직접 코드를 떠올리며 본인의 답을 만들어보세요.
1. "설계도와 실체를 굳이 둘로 나누는 이유는?"
오늘 우리는 클래스(설계도)를 한 번 그려두고, new 로 객체(실체)를 여섯 개 찍어냈어요.
설계도는 하나인데 실체는 여러 개죠.
만약 설계도 없이, 회원 한 명 한 명을 매번 처음부터 따로 정의해야 한다면 어떨까요?
반대로, 회원이 백만 명이어도 설계도는 여전히 Member 하나면 충분하다는 건 무슨 의미일까요?
"틀 하나로 실체 여러 개"라는 구조가 왜 편리하고 안전한지, 메모리와 유지보수 관점에서 떠올려보세요.
2. "점수 계산은 왜 Member 밖에 있을까?"
오늘 calculateRecommendScore(Member member) 는 Member 객체를 받아 점수를 계산했어요.
그런데 잘 보면 이 메서드는 Member 클래스 밖(MemberDemo)에 떨어져 있어요.
회원의 점수를 계산하는 일인데, 정작 회원 클래스 안엔 없는 거죠.
만약 이 메서드가 Member 클래스 안에 있어서 member.calculateRecommendScore() 처럼 부를 수 있다면 어떨까요?
데이터(필드)와 그 데이터를 다루는 행동(메서드)이 한 클래스 안에 같이 있는 것과,
지금처럼 따로 떨어져 있는 것 — 어느 쪽이 더 자연스럽고, 어느 쪽이 더 안전할까요?
(이건 다음 시간 인스타그램의 핵심 주제이기도 해요. 미리 본인 생각을 그려두면 좋아요.)
3. "객체 배열의 빈 칸을 건드리면?"
오늘 Member[] members = { new Member(...), ... } 처럼 배열을 만들면서 모든 칸을 객체로 채웠어요.
그런데 만약 Member[] members = new Member[6]; 처럼 칸만 여섯 개 만들고 객체는 안 채우면 어떻게 될까요?
이때 각 칸은 아무 객체도 안 가리키는 null 상태예요. (Step 3의 기본값 기억나시죠?)
이 상태에서 members[0].username 을 꺼내려 하면 무슨 일이 벌어질까요?
"아무것도 안 가리키는 변수의 점(.)을 찍는다"는 게 왜 위험한지,
그리고 객체를 다룰 때 "이 변수가 진짜 객체를 가리키고 있나?"를 왜 늘 신경 써야 하는지 생각해보세요.
✅ 예시 답안정답 보기
본인 답안을 먼저 작성한 뒤 비교해보세요. 정답이 하나만 있는 건 아니에요. 여기 답안은 모범 사례 중 하나일 뿐, 본인만의 더 깔끔한 풀이가 있다면 그게 답입니다.
세 과제 모두 오늘 만든 Member 클래스와 객체 배열을 활용해요. 같은 패키지(com.instagram.javabasic.domain.member)에 새 클래스/메서드를 붙여 연습하면 됩니다.
🎯 [과제 1 예시 답안] Member에 정보 한 칸 추가하기
Member 클래스에 "비공개 계정 여부"를 담는 boolean isPrivate 필드를 추가하고, 비공개 계정이면 이름 뒤에 🔒 를 붙여 출력하는 과제입니다.
핵심 접근
평행 배열이었다면 boolean[] isPrivate 배열을 새로 만들고, 그 배열을 받는 메서드를 전부 고쳐야 했어요. 클래스에선 칸 하나 + 생성자 한 줄이면 끝나죠. 이 차이를 직접 느끼는 게 이 과제의 핵심이에요. 필드를 추가하면 생성자의 매개변수도 함께 늘려, 객체를 만드는 순간 그 값까지 한 번에 채우면 됩니다.
예시 코드
먼저 Member 클래스에 필드와 생성자 매개변수를 한 칸씩 늘립니다.
// com/instagram/javabasic/domain/member/Member.java
public class Member {
String username;
int followers;
int posts;
int mutualFriends;
int daysActive;
boolean isPrivate; // 👈 추가한 칸 하나
public Member() {
}
// 매개변수도 마지막에 한 칸 늘려요
public Member(String username, int followers, int posts,
int mutualFriends, int daysActive, boolean isPrivate) {
this.username = username;
this.followers = followers;
this.posts = posts;
this.mutualFriends = mutualFriends;
this.daysActive = daysActive;
this.isPrivate = isPrivate; // 👈 추가한 줄 하나
}
}
출력하는 쪽에서는 if 로 비공개 여부를 확인해 🔒 를 붙입니다.
static void printAllMembers(Member[] members) {
for (int i = 0; i < members.length; i++) {
Member member = members[i];
String lock = "";
if (member.isPrivate) {
lock = " 🔒";
}
System.out.println("@" + member.username + lock
+ " 팔로워 " + member.followers);
}
}
main 에서 회원을 만들 때 마지막 인자로 공개/비공개 여부를 넘겨요.
Member[] members = {
new Member("jaehoon_dev", 1240, 42, 8, 120, false),
new Member("seungwoo", 320, 12, 2, 30, true)
};
printAllMembers(members);
실행 결과:
@jaehoon_dev 팔로워 1240
@seungwoo 🔒 팔로워 320
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 필드 추가 | boolean isPrivate; 를 클래스 안에 한 칸 추가했는가 |
높음 |
| 생성자 확장 | 매개변수에 boolean isPrivate 를 추가하고 this.isPrivate = isPrivate; 로 채웠는가 |
높음 |
| 조건 분기 출력 | if (member.isPrivate) 로 비공개일 때만 🔒 를 붙였는가 |
중간 |
| 객체 생성 | new Member(..., true/false) 로 마지막 인자를 넘겼는가 |
중간 |
흔한 실수
- 생성자만 고치고 필드를 안 만든 경우 — 생성자 본문에서
this.isPrivate를 쓰려면 클래스에isPrivate칸이 먼저 있어야 해요. "필드 선언 → 생성자 매개변수 → 본문 대입" 세 군데가 짝을 이뤄야 합니다. - 기본 생성자 깜빡 — 매개변수 생성자만 남기고 기본 생성자를 지우면,
new Member()를 쓰던 다른 코드가 깨질 수 있어요. 두 생성자를 같이 두면 안전해요.
실무 개선 포인트 (심화)
지금은 비공개일 때 🔒 를 붙이는 로직이 printAllMembers 안에 들어 있어요. 그런데 "이 회원을 화면에 어떻게 표시할까?"는 사실 회원 스스로가 가장 잘 아는 정보죠. 다음 시간에 배울 인스턴스 메서드를 쓰면, member.displayName() 처럼 회원 객체가 자기 표시 이름(🔒 포함)을 직접 만들어주도록 옮길 수 있어요. 데이터(isPrivate)와 그 데이터를 쓰는 행동(표시 이름 만들기)이 한 클래스에 모이는 거죠. 오늘은 "아, 이 로직이 밖에 있으니 좀 어색하네" 정도만 느껴두면 충분해요.
🎯 [과제 2 예시 답안] 새 설계도 만들기 — Post 클래스
회원이 아니라 "게시물"의 설계도 Post 클래스를 처음부터 직접 만들어보는 과제입니다.
핵심 접근
Member 를 만든 과정을 그대로 따라가면 돼요. 필드 → 기본 생성자 → 매개변수 생성자 순서요. 설계도를 한 번 더 직접 그려보면 "클래스는 이렇게 만드는 거구나"가 손에 익어요. 그리고 게시물 여러 개는 Post[] 객체 배열에 담아 for 루프로 순회하면 됩니다.
예시 코드
// com/instagram/javabasic/domain/post/Post.java
public class Post {
String content; // 게시물 내용
String authorName; // 작성자 이름
int likeCount; // 좋아요 수
public Post() {
}
public Post(String content, String authorName, int likeCount) {
this.content = content;
this.authorName = authorName;
this.likeCount = likeCount;
}
}
데모용 main 에서 객체 배열로 담아 순회 출력합니다.
public static void main(String[] args) {
Post[] posts = {
new Post("오늘 카페 다녀왔어요", "마이멜로디", 12),
new Post("새 필터 써봤어요", "쿠로미", 30)
};
for (int i = 0; i < posts.length; i++) {
Post post = posts[i];
System.out.println("[" + post.authorName + "] " + post.content
+ " (좋아요 " + post.likeCount + "개)");
}
}
실행 결과:
[마이멜로디] 오늘 카페 다녀왔어요 (좋아요 12개)
[쿠로미] 새 필터 써봤어요 (좋아요 30개)
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 필드 세 개 | content, authorName, likeCount 를 적절한 타입으로 선언했는가 |
높음 |
| 생성자 두 개 | 기본 생성자 + 매개변수 생성자를 모두 만들고 this 로 채웠는가 |
높음 |
| 객체 배열 | Post[] posts = { new Post(...), ... } 로 여러 게시물을 담았는가 |
중간 |
| 순회 출력 | for 루프로 돌며 post.필드 를 점(.)으로 꺼냈는가 |
중간 |
흔한 실수
String likeCount처럼 타입을 잘못 고른 경우 — 좋아요 수는 셈을 할 숫자라int가 맞아요. 출력만 보면String이어도 화면엔 똑같이 나오지만, 나중에 "좋아요 +1" 같은 계산을 하려면int여야 해요.- 생성자 본문에서
content = content;처럼this를 빠뜨린 경우 — 매개변수와 필드 이름이 같을 때this가 없으면 자바는 둘 다 매개변수로 보고, 필드는 영영 안 채워져요. 같은 이름일 땐 왼쪽에this.를 꼭 붙이세요.
실무 개선 포인트 (심화)
지금 Post 는 작성자를 String authorName 으로만 들고 있어요. 그런데 작성자는 사실 우리가 만든 Member 객체예요. 실무에서는 String authorName 대신 Member author 처럼 객체가 다른 객체를 필드로 품는 설계를 자주 써요. 그러면 게시물에서 작성자의 팔로워 수, 비공개 여부까지 post.author.followers 로 한 번에 따라갈 수 있죠. "객체 안에 객체"라는 구조는 앞으로 도메인을 엮을 때 계속 만나게 돼요. 오늘은 설계도 하나를 스스로 그렸다는 것만으로 충분합니다.
🎯 [과제 3 예시 답안] 가장 활발한 회원 찾기
활동 일수(daysActive)가 가장 큰 회원 객체를 돌려주는 findMostActive 메서드를 만드는 과제입니다. 인덱스가 아니라 객체 자체를 반환하는 게 포인트예요.
핵심 접근
"가장 큰 값의 위치 찾기"는 Day 5·Day 7에서 해본 패턴이에요. 다만 이번엔 인덱스(int)가 아니라 Member 객체를 반환해요. Member maxMember = members[0]; 로 첫 회원을 일단 후보로 두고, 배열을 돌며 더 활발한 회원을 만나면 후보를 갈아끼우면 됩니다. 메서드가 객체를 반환할 수 있다는 게 오늘의 새로운 점이에요.
예시 코드
// com/instagram/javabasic/domain/member/MemberDemo.java 에 추가
static Member findMostActive(Member[] members) {
Member maxMember = members[0]; // 일단 첫 회원을 후보로
for (int i = 1; i < members.length; i++) {
if (members[i].daysActive > maxMember.daysActive) {
maxMember = members[i]; // 더 활발한 회원을 만나면 후보 교체
}
}
return maxMember; // 회원 '객체' 를 그대로 반환
}
main 에서 불러 출력합니다.
Member top = findMostActive(members);
System.out.println("가장 활발한 회원: @" + top.username
+ " (활동 " + top.daysActive + "일)");
실행 결과:
가장 활발한 회원: @wooseok99 (활동 500일)
여섯 명 중 활동 일수가 가장 큰 사람은 wooseok99(500일)예요. 반환된 게 Member 객체라서, top.username 과 top.daysActive 를 점(.)으로 바로 꺼내 쓸 수 있죠.
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 반환 타입 | 메서드 반환 타입이 Member 인가 (인덱스 int 가 아니라 객체) |
높음 |
| 최댓값 탐색 | 후보를 두고 daysActive 가 더 큰 객체로 교체하는 패턴을 썼는가 |
높음 |
| 첫 후보 초기화 | members[0] 으로 후보를 시작하고 i = 1 부터 비교했는가 |
중간 |
| 객체 활용 | 반환된 객체에서 top.username 등을 점(.)으로 꺼냈는가 |
중간 |
흔한 실수
- 빈 배열을 안 챙긴 경우 —
members[0]으로 시작하니, 배열이 비어 있으면 그 줄에서 에러가 나요. 실무라면 맨 앞에 "배열이 비었으면 어떻게 할지"를 정해둬야 하지만, 오늘 데이터는 항상 여섯 명이라 그냥 둬도 동작은 해요. - 인덱스를 반환해버린 경우 —
findTopFollowers처럼int를 돌려주면, 부르는 쪽에서 다시members[그 인덱스]로 객체를 꺼내야 해요. 이 과제는 객체를 바로 돌려주는 연습이라 반환 타입이Member인 게 핵심이에요.
실무 개선 포인트 (심화)
도전 과제처럼 활동 일수가 똑같은 회원이 둘이면, 위 코드는 먼저 나온 사람을 뽑아요. 비교 조건이 > (초과)라서 같은 값일 땐 후보를 안 바꾸거든요. 만약 "나중에 나온 사람"을 뽑고 싶다면 >= (이상)로 바꾸면 돼요. 이렇게 "같을 때 누구를 고를지(tie-breaking)"를 코드 한 글자(> vs >=)가 결정한다는 걸 알아두면, 나중에 정렬·순위 로직에서 헷갈릴 일이 줄어요.
🤔 생각해볼 주제 예시답안
1. 설계도와 실체를 굳이 둘로 나누는 이유는?
[문제 상황 요약]
클래스(설계도)는 한 번만 그려두고, new 로 객체(실체)를 여러 개 찍어냈어요. 설계도는 하나인데 실체는 여섯 개죠. "왜 굳이 설계도와 실체를 따로 둘까? 그냥 회원 한 명 한 명을 매번 정의하면 안 될까?" 하는 질문이에요.
[튜터의 가이드 및 해설]
핵심은 "규칙은 한 곳에, 데이터는 여러 개" 예요.
만약 설계도 없이 회원 한 명 한 명을 매번 처음부터 정의한다면, "회원은 이름·팔로워·게시물을 가진다"는 똑같은 규칙을 백만 번 반복해서 써야 해요. 그러다 "팔로워 칸을 추가하자"가 되면 백만 군데를 다 고쳐야 하죠. 설계도가 하나면, 규칙을 바꿀 때 설계도 한 곳만 고치면 모든 객체에 똑같이 반영돼요.
메모리 관점도 봅시다. 설계도(클래스)는 "어떤 칸을 가지는지"에 대한 정보라 프로그램에 한 벌만 있으면 돼요. 실제로 메모리를 차지하는 건 new 로 찍어낸 객체들이고요. 그래서 회원이 백만 명이어도 설계도는 여전히 Member 하나, 늘어나는 건 Heap의 객체들뿐이에요. "틀 하나로 붕어빵 백만 개"가 낭비가 아니라 효율인 이유예요.
이게 객체지향의 출발점이에요. 공통된 규칙(구조와 행동)은 클래스에 한 번 정의하고, 서로 다른 데이터는 객체마다 따로 담는 거죠.
🎯 면접관을 홀리는 핵심 멘트
"클래스는 '규칙을 적는 곳', 객체는 '데이터를 담는 곳'입니다. 규칙을 한 군데에 모아두면 수정도 한 군데에서 끝나고, 같은 설계도로 객체를 얼마든지 찍어내도 메모리에 새로 쌓이는 건 데이터뿐이라 효율적입니다."
2. 점수 계산은 왜 Member 밖에 있을까?
[문제 상황 요약]
오늘 calculateRecommendScore(Member member) 는 Member 객체를 받아 점수를 계산했어요. 그런데 이 메서드는 Member 클래스 밖(MemberDemo)에 떨어져 있죠. "회원의 점수인데 왜 회원 클래스 안엔 없을까? 안에 있으면 더 자연스럽지 않을까?" 하는 질문이에요.
[튜터의 가이드 및 해설]
아주 날카로운 관찰이에요. 사실 이 어색함이 다음 시간으로 가는 다리예요.
지금 Member 는 데이터(필드)만 가지고 있고, 그 데이터로 무언가 하는 행동(메서드) 은 전부 바깥에 있어요. 이걸 비유하면, 자동차의 부품(엔진·바퀴)은 차 안에 있는데 "달리기"라는 기능은 차 밖 어딘가에 따로 떨어져 있는 셈이에요. 좀 이상하죠?
만약 점수 계산이 Member 클래스 안에 있어서 member.calculateScore() 처럼 부를 수 있다면, "회원 객체가 자기 점수를 스스로 계산한다"가 돼요. 데이터(followers, posts…)와 그걸 쓰는 행동(점수 계산)이 한 클래스에 모이는 거죠. 그러면 점수 공식을 바꿀 때도 Member 안만 보면 되고, 다른 사람이 코드를 읽을 때도 "회원에 관한 건 회원 클래스에 다 있구나" 하고 한 군데서 찾을 수 있어요.
이렇게 데이터와 행동을 한 클래스에 묶는 것이 객체지향의 핵심이에요. 오늘은 데이터만 묶었고, 다음 시간엔 행동까지 묶어 진짜 "객체다운 객체"를 만들 거예요.
🎯 면접관을 홀리는 핵심 멘트
"데이터를 가진 객체가 그 데이터를 다루는 행동까지 함께 가지는 게 객체지향의 핵심입니다. 점수 계산을
Member밖에 두면 데이터와 로직이 흩어지지만,member.calculateScore()처럼 객체 안에 넣으면 '회원에 관한 모든 것은 회원 클래스에' 라는 응집도가 생깁니다."
3. 객체 배열의 빈 칸을 건드리면?
[문제 상황 요약]
오늘은 Member[] members = { new Member(...), ... } 로 모든 칸을 객체로 채웠어요. 그런데 Member[] members = new Member[6]; 처럼 칸만 만들고 객체는 안 채우면, 각 칸은 null 상태예요. 이때 members[0].username 을 꺼내면 무슨 일이 벌어질까요?
[튜터의 가이드 및 해설]
결론부터 말하면, 프로그램이 에러를 내며 멈춰요. NullPointerException 이라는 에러예요.
왜 그럴까요? Step 3에서 봤듯이, 참조 변수는 "객체의 주소"만 들고 있어요. 그런데 null 은 "아무 주소도 안 가지고 있다", 즉 "아무 객체도 안 가리킨다"는 뜻이에요. 빈 손인 거죠.
members[0].username 의 점(.)은 "이 객체의 username 칸을 보여줘"라는 명령이에요. 그런데 members[0] 이 null 이면 가리키는 객체가 아예 없으니, 자바는 "보여줄 객체가 없는데요?" 하며 NullPointerException 을 던져요. 빈 주소를 따라가려다 길을 잃는 셈이에요.
members[0] = null
│
▼ .username 을 찾아가려는데…
(가리키는 객체가 없음 → NullPointerException)
그래서 객체를 다룰 땐 "이 변수가 진짜 객체를 가리키고 있나?" 를 늘 신경 써야 해요. 특히 배열을 new Member[6] 으로 칸만 만들었을 땐, 점(.)을 찍기 전에 객체를 채웠는지 확인하는 습관이 중요해요. 이 NullPointerException 은 초보자가 가장 많이 만나는 에러 중 하나라, 지금 원리를 잡아두면 앞으로 훨씬 덜 당황하게 돼요.
🎯 면접관을 홀리는 핵심 멘트
"
null은 '아무 객체도 안 가리킨다'는 뜻이고, 그 상태에서 점(.)으로 멤버에 접근하면NullPointerException이 납니다. 참조 변수를 쓸 땐 '이게 진짜 객체를 가리키고 있는가'를 늘 확인하는 게 객체를 안전하게 다루는 첫걸음입니다."