본문 바로가기
언어 공부/JAVA

JAVA D5 [ 클래스 ] - 실무에서 바로 사용 가능한 핵심 포인트

by TMrare 2025. 3. 8.
클래스 실무 정리

클래스 실무 정리

1. 클래스와 객체의 기본 개념

클래스 (Class)
  • 객체를 생성하기 위한 설계도 또는
  • 객체가 가져야 할 속성(변수)기능(메서드)을 정의
  • 실제 메모리 공간을 차지하지 않음
  • 예: Student, Car, Account
객체 (Object)
  • 클래스를 기반으로 생성된 실체
  • 클래스에서 정의한 속성과 기능을 가진 메모리 상의 실제 데이터
  • 독립적인 상태를 가짐
  • 예: student1, myCar, savingsAccount
인스턴스 (Instance)
  • 특정 클래스로부터 생성된 객체
  • 클래스와의 관계를 강조할 때 사용하는 용어
  • 모든 인스턴스는 객체이지만, 특정 클래스와의 관계를 명확히 할 때 인스턴스라는 용어 사용
  • 예: student1Student 클래스의 인스턴스

객체 vs 인스턴스

둘 다 클래스에서 나온 실체라는 점에서 비슷하게 사용됨

  • student1은 객체다 (일반적인 표현)
  • student1Student의 인스턴스다 (특정 클래스와의 관계 강조)

모든 인스턴스는 객체지만, 인스턴스라는 용어는 해당 객체가 특정 클래스로부터 생성되었음을 강조할 때 사용

중요한 주의사항

변수에는 인스턴스 자체가 아닌 인스턴스의 참조값(주소)이 저장됨!

대입(=) 연산 시 인스턴스가 복사되는 것이 아니라 참조값만 복사됨

2. 클래스 생성과 객체 사용 단계

1
변수 선언
Student student1; // Student 타입 변수 선언
  • Student 타입을 받을 수 있는 변수를 선언
  • int가 정수, String이 문자열을 담을 수 있듯이, Student는 Student 타입의 객체(인스턴스)를 담는 변수
  • 이 단계에서는 아직 실제 객체가 생성되지 않음 (변수만 선언된 상태)
2
객체 생성
student1 = new Student(); // Student 인스턴스 생성
  • 객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 함
  • new Student()는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 의미
  • 메모리에 실제 Student 객체(인스턴스)가 생성됨
  • Student 클래스에 정의된 멤버 변수(String name, int age, int grade 등)를 저장할 메모리 공간도 함께 확보
3
참조값 보관
  • 객체를 생성하면, 자바는 메모리에 있는 객체에 접근할 수 있는 참조값(주소)을 반환
  • 이 참조값은 student1 변수에 저장됨
  • student1 변수는 이제 실제 Student 객체의 참조값을 가지고 있어 객체에 접근 가능
스택 메모리

student10x1234

힙 메모리 (0x1234)

Student 객체

name: null

age: 0

grade: 0

4
객체 사용
// 객체의 필드에 값 대입 student1.name = "홍길동"; student1.age = 16; student1.grade = 1; // 객체의 메서드 호출 student1.study(); String info = student1.getInfo();
  • 객체에 접근하려면 .(점, dot) 연산자를 사용
  • 점 연산자는 변수에 저장된 참조값을 통해 메모리에 존재하는 실제 객체에 접근
  • 객체의 필드(멤버 변수)에 값을 대입하거나 읽어올 수 있음
  • 객체의 메서드를 호출하여 기능을 사용할 수 있음

참조값을 변수에 보관해야 하는 이유

객체를 생성하는 new Student() 코드 자체에는 이름이 없음. 이 코드는 단순히 Student 클래스를 기반으로 메모리에 실제 객체를 생성할 뿐임

생성된 객체에 접근하려면 그 객체의 참조값(주소)을 어딘가에 보관해야 함

참조값을 통해서만 실제 메모리에 존재하는 객체에 접근할 수 있음

3. 객체 생성과 사용 예제

Student 클래스 정의
public class Student { // 멤버 변수 (필드) String name; int age; int grade; // 메서드 void study() { System.out.println(name + "이(가) 공부합니다."); } String getInfo() { return "이름: " + name + ", 나이: " + age + ", 학년: " + grade; } }
Student 객체 생성 및 사용
// 1단계 + 2단계: 변수 선언과 객체 생성을 한 번에 Student student1 = new Student(); // 3단계: 참조값이 student1 변수에 자동으로 저장됨 // 4단계: 객체 사용 student1.name = "홍길동"; student1.age = 16; student1.grade = 1; // 메서드 호출 student1.study(); // 출력: 홍길동이(가) 공부합니다. String info = student1.getInfo(); System.out.println(info); // 출력: 이름: 홍길동, 나이: 16, 학년: 1 // 두 번째 객체 생성 및 사용 Student student2 = new Student(); student2.name = "김철수"; student2.age = 17; student2.grade = 2; System.out.println(student2.getInfo()); // 출력: 이름: 김철수, 나이: 17, 학년: 2

4. 객체 참조와 복사 이해하기

참조값 복사 vs 객체 복사
// 참조값 복사 Student student1 = new Student(); student1.name = "홍길동"; Student student2 = student1; // student1의 참조값을 student2에 복사 System.out.println(student2.name); // 출력: 홍길동 // student2를 통해 객체 수정 (student1도 같은 객체를 가리키므로 영향 받음) student2.name = "이름변경"; System.out.println(student1.name); // 출력: 이름변경

참조값 복사 시 메모리 구조

스택 메모리

student10x1234

student20x1234

힙 메모리 (0x1234)

Student 객체

name: "이름변경"

age: 0

grade: 0

student1과 student2가 같은 힙 메모리 영역의 객체를 참조

객체 복사 방법 (깊은 복사)

실제 객체의 내용을 복사하여 별도의 객체를 만들려면 명시적으로 새 객체를 생성하고 내용을 복사해야 함

// 깊은 복사 (Deep Copy) Student student1 = new Student(); student1.name = "홍길동"; student1.age = 16; student1.grade = 1; // 새로운 객체를 생성하고 필드값 복사 Student student3 = new Student(); student3.name = student1.name; student3.age = student1.age; student3.grade = student1.grade; // student3 객체 수정은 student1에 영향을 주지 않음 student3.name = "이름변경"; System.out.println(student1.name); // 출력: 홍길동 System.out.println(student3.name); // 출력: 이름변경

5. 생성자와 초기화

생성자 (Constructor)

객체 생성 시 초기화를 담당하는 특별한 메서드

  • 클래스 이름과 동일한 이름을 가짐
  • 반환 타입이 없음 (void도 표시하지 않음)
  • 객체가 생성될 때 자동으로 호출됨
생성자 정의 및 사용
public class Student { // 멤버 변수 (필드) String name; int age; int grade; // 기본 생성자 (매개변수 없음) public Student() { System.out.println("학생 객체가 생성되었습니다."); } // 매개변수가 있는 생성자 public Student(String name, int age, int grade) { this.name = name; // this는 현재 객체를 가리킴 this.age = age; this.grade = grade; System.out.println("이름, 나이, 학년이 초기화된 학생 객체가 생성되었습니다."); } // 메서드 void study() { System.out.println(name + "이(가) 공부합니다."); } String getInfo() { return "이름: " + name + ", 나이: " + age + ", 학년: " + grade; } } // 사용 예시 public class Main { public static void main(String[] args) { // 기본 생성자 사용 Student student1 = new Student(); student1.name = "홍길동"; student1.age = 16; student1.grade = 1; // 매개변수가 있는 생성자 사용 (객체 생성과 동시에 초기화) Student student2 = new Student("김철수", 17, 2); System.out.println(student1.getInfo()); System.out.println(student2.getInfo()); } }

생성자 특징

  • 생성자를 정의하지 않으면 컴파일러가 자동으로 기본 생성자(매개변수 없는 생성자)를 추가함
  • 매개변수가 있는 생성자를 정의하면 기본 생성자는 자동으로 추가되지 않음
  • 필요에 따라 여러 개의 생성자를 정의할 수 있음 (생성자 오버로딩)
  • this 키워드는 현재 객체를 가리킴 (주로 매개변수 이름과 필드 이름이 같을 때 구분하기 위해 사용)

6. 접근 제어자와 캡슐화

접근 제어자 (Access Modifier)

클래스의 멤버(변수, 메서드)에 대한 접근 범위를 제한하는 키워드

접근 제어자 접근 범위 설명
private 클래스 내부 같은 클래스 내에서만 접근 가능
default (없음) 같은 패키지 같은 패키지 내의 클래스에서만 접근 가능
protected 같은 패키지 + 상속 같은 패키지 및 상속받은 클래스에서 접근 가능
public 모든 클래스 어디서든 접근 가능
캡슐화를 적용한 Student 클래스
public class Student { // 멤버 변수를 private으로 선언하여 외부에서 직접 접근 불가 private String name; private int age; private int grade; // 생성자 public Student(String name, int age, int grade) { this.name = name; setAge(age); // 유효성 검사를 위해 setter 사용 setGrade(grade); } // getter 메서드 - 데이터 읽기 public String getName() { return name; } public int getAge() { return age; } public int getGrade() { return grade; } // setter 메서드 - 데이터 쓰기 (유효성 검사 포함) public void setName(String name) { this.name = name; } public void setAge(int age) { if (age > 0 && age < 120) { // 유효성 검사 this.age = age; } else { System.out.println("유효하지 않은 나이입니다."); } } public void setGrade(int grade) { if (grade >= 1 && grade <= 6) { // 유효성 검사 this.grade = grade; } else { System.out.println("유효하지 않은 학년입니다."); } } // 기타 메서드 public void study() { System.out.println(name + "이(가) 공부합니다."); } public String getInfo() { return "이름: " + name + ", 나이: " + age + ", 학년: " + grade; } }

캡슐화(Encapsulation)의 이점

  • 데이터 보호: 외부에서 직접 데이터 접근 불가
  • 유효성 검사: setter 메서드를 통해 데이터 유효성 검사 가능
  • 유지보수 용이성: 내부 구현을 변경해도 외부에 영향 최소화
  • 코드 일관성: 데이터 접근 방법 통일

7. 정적(static) 멤버

static 키워드

객체가 아닌 클래스에 속하는 멤버를 정의할 때 사용

  • 정적 변수(클래스 변수): 모든 객체가 공유하는 변수
  • 정적 메서드(클래스 메서드): 객체 생성 없이 호출 가능한 메서드
static 멤버 사용 예제
public class Student { // 인스턴스 변수 (객체마다 별도로 가짐) private String name; private int age; private int grade; // 정적 변수 (모든 Student 객체가 공유) private static int totalCount = 0; private static final String SCHOOL_NAME = "자바고등학교"; // 상수 // 생성자 public Student(String name, int age, int grade) { this.name = name; this.age = age; this.grade = grade; totalCount++; // 학생 객체가 생성될 때마다 증가 } // 정적 메서드 public static int getTotalCount() { return totalCount; } public static String getSchoolName() { return SCHOOL_NAME; } // 인스턴스 메서드 public String getInfo() { return "이름: " + name + ", 나이: " + age + ", 학년: " + grade; } } // 사용 예시 public class Main { public static void main(String[] args) { // 정적 메서드/변수는 객체 생성 없이 사용 가능 System.out.println("학교 이름: " + Student.getSchoolName()); System.out.println("학생 수: " + Student.getTotalCount()); // 0 // 학생 객체 생성 Student student1 = new Student("홍길동", 16, 1); Student student2 = new Student("김철수", 17, 2); // 모든 객체가 공유하는 정적 변수 확인 System.out.println("학생 수: " + Student.getTotalCount()); // 2 } }

static 사용 시 주의사항

  • 정적 메서드에서는 인스턴스 변수/메서드를 직접 사용할 수 없음
  • 정적 메서드에서는 this를 사용할 수 없음
  • 객체 상태와 관련 없는 기능을 구현할 때 주로 사용 (유틸리티 메서드 등)
  • 상수(변하지 않는 값)는 static final로 선언하는 것이 일반적

8. 상속과 다형성

상속 (Inheritance)

기존 클래스의 속성과 기능을 새로운 클래스가 물려받는 것

  • 코드 재사용성 증가
  • 부모 클래스(상위 클래스)의 기능을 확장하거나 수정 가능
  • 자바에서는 extends 키워드를 사용하여 상속 구현
  • 자바는 단일 상속만 지원 (다중 상속 불가)
상속 예제
// 부모 클래스 (상위 클래스) public class Person { protected String name; protected int age; public Person(String name, int age) { this.name = name; this.age = age; } public void introduce() { System.out.println("안녕하세요. 제 이름은 " + name + "이고, " + age + "살입니다."); } } // 자식 클래스 (하위 클래스) public class Student extends Person { private int grade; public Student(String name, int age, int grade) { super(name, age); // 부모 클래스의 생성자 호출 this.grade = grade; } // 메서드 오버라이딩 (부모 클래스의 메서드를 재정의) @Override public void introduce() { System.out.println("안녕하세요. 저는 " + grade + "학년 학생 " + name + "이고, " + age + "살입니다."); } // 자식 클래스만의 메서드 public void study() { System.out.println(name + "이(가) 공부합니다."); } }

다형성 (Polymorphism)

하나의 객체가 여러 타입을 가질 수 있는 특성

  • 부모 타입의 참조 변수로 자식 객체를 참조 가능
  • 메서드 오버라이딩을 통해 동일한 메서드 호출도 객체마다 다른 동작 가능
  • 코드의 유연성과 확장성 증가
다형성 예제
public class Main { public static void main(String[] args) { // 다형성: 부모 타입 변수로 자식 객체 참조 Person p1 = new Person("홍길동", 30); Person p2 = new Student("김철수", 16, 1); // Student는 Person의 하위 타입 // 메서드 호출 - 동일한 메서드 호출이지만 실제 객체 타입에 따라 다른 동작 p1.introduce(); // Person의 introduce() 호출 p2.introduce(); // Student의 introduce() 호출 (오버라이딩됨) // 부모 타입으로는 자식 클래스의 고유 메서드 직접 호출 불가 // p2.study(); // 컴파일 에러 // 타입 캐스팅을 통해 자식 클래스의 메서드 호출 if (p2 instanceof Student) { Student s = (Student) p2; s.study(); } } }

9. 추상 클래스와 인터페이스

추상 클래스 (Abstract Class)

  • abstract 키워드로 선언
  • 직접 인스턴스화할 수 없음
  • 추상 메서드와 일반 메서드 모두 가질 수 있음
  • 상속을 통해 자식 클래스가 추상 메서드를 구현해야 함
  • 변수, 생성자 정의 가능

인터페이스 (Interface)

  • interface 키워드로 선언
  • 모든 메서드가 기본적으로 추상 메서드 (Java 8부터 default, static 메서드 가능)
  • 모든 변수는 자동으로 public static final
  • 다중 구현 가능 (클래스는 여러 인터페이스 구현 가능)
  • 생성자 정의 불가
추상 클래스 예제
// 추상 클래스 public abstract class Shape { protected String color; public Shape(String color) { this.color = color; } // 추상 메서드 (자식 클래스에서 구현해야 함) public abstract double getArea(); // 일반 메서드 public String getColor() { return color; } } // 추상 클래스 구현 public class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double getArea() { return Math.PI * radius * radius; } }
인터페이스 예제
// 인터페이스 정의 public interface Drawable { void draw(); // 추상 메서드 // Java 8부터 가능한 default 메서드 default void printInfo() { System.out.println("Drawable 객체입니다."); } } // 인터페이스 구현 public class Rectangle extends Shape implements Drawable { private double width; private double height; public Rectangle(String color, double width, double height) { super(color); this.width = width; this.height = height; } @Override public double getArea() { return width * height; } @Override public void draw() { System.out.println(color + " 색상의 사각형을 그립니다."); } }

10. 실무에서 자주 사용되는 클래스 패턴

DTO (Data Transfer Object) 패턴

데이터를 전달하기 위한 객체 패턴

public class UserDTO { private long id; private String username; private String email; private String address; // 생성자, Getter, Setter 메서드 // Builder 패턴 (선택적) public static class Builder { private long id; private String username; private String email; private String address; public Builder id(long id) { this.id = id; return this; } public Builder username(String username) { this.username = username; return this; } // 나머지 setter public UserDTO build() { return new UserDTO(this); } } private UserDTO(Builder builder) { this.id = builder.id; this.username = builder.username; this.email = builder.email; this.address = builder.address; } }

싱글톤 (Singleton) 패턴

클래스의 인스턴스가 하나만 생성되도록 하는 패턴

public class DatabaseConnection { // 유일한 인스턴스를 저장할 정적 변수 private static DatabaseConnection instance; // 외부에서 생성자 호출 방지 (private) private DatabaseConnection() { // 초기화 코드 } // 인스턴스 접근을 위한 정적 메서드 public static DatabaseConnection getInstance() { if (instance == null) { instance = new DatabaseConnection(); } return instance; } // 데이터베이스 연결 메서드 public void connect() { System.out.println("데이터베이스에 연결합니다."); } public void disconnect() { System.out.println("데이터베이스 연결을 종료합니다."); } }

팩토리 (Factory) 패턴

객체 생성을 캡슐화하여 서브클래스에서 어떤 객체를 생성할지 결정하는 패턴

// 상품 인터페이스 public interface Product { void use(); } // 구체적인 상품 클래스들 public class ConcreteProductA implements Product { @Override public void use() { System.out.println("상품 A를 사용합니다."); } } public class ConcreteProductB implements Product { @Override public void use() { System.out.println("상품 B를 사용합니다."); } } // 팩토리 클래스 public class ProductFactory { public static Product createProduct(String type) { if ("A".equals(type)) { return new ConcreteProductA(); } else if ("B".equals(type)) { return new ConcreteProductB(); } throw new IllegalArgumentException("Unknown product type: " + type); } }

11. 결론 및 실무 체크리스트

클래스 설계 체크리스트

  • 클래스 이름이 명확하고 단일 책임 원칙을 준수하는가?
  • 모든 필드(멤버 변수)가 적절한 접근 제어자로 선언되었는가?
  • 필요한 생성자가 정의되어 있는가?
  • Getter/Setter가 필요한 경우 모두 구현되었는가?
  • 메서드 이름이 행동을 명확히 표현하는가?
  • 불변성(Immutability)이 필요한 경우 적절히 구현되었는가?
  • 상속/구현 관계가 적절히 설계되었는가?
  • 코드 중복이 최소화되었는가?
  • 클래스 내부 구현이 적절히 캡슐화되었는가?

Java 클래스 작성 모범 사례

  1. 필드는 private으로 선언하여 캡슐화
  2. 적절한 toString(), equals(), hashCode() 메서드 구현 (필요시)
  3. 불변 객체는 final 필드와 방어적 복사 사용
  4. 상속보다 컴포지션 선호 (Has-a 관계가 Is-a 관계보다 유연)
  5. 인터페이스를 통한 다형성 활용
  6. 예외 처리를 통한 견고한 클래스 작성
  7. 클래스 문서화 (Javadoc 주석 활용)