[Spring] 0. 객체지향 프로그래밍
객체지향 프로그래밍 (Object-Oriented Programming)
Spring 프레임워크를 깊이 있게 이해하려면, 그 뿌리가 되는 객체지향 프로그래밍 개념을 먼저 알아야 한다.
객체지향 프로그래밍이란?
- 프로그램을 객체 단위로 구성하여 코드의 재사용성과 유지보수성을 높이는 방법론
- 객체는 데이터(속성)와 기능(메서드)을 하나로 묶은 단위이다.
- 객체지향 프로그래밍에서는 객체들이 서로 협력하며 문제를 해결한다.
객체지향 설계의 장점
- 재사용성 : 상속과 모듈화를 통해 코드를 재사용 가능.
- 유지보수성 : 캡슐화로 내부 구현 변경이 외부에 영향을 미치지 않음.
- 확장성 : 다형성과 추상화를 통해 새로운 기능을 쉽게 추가 가능.
객체지향의 4대 핵심 원칙
- 캡슐화(Encapsulation) : 데이터와 해당 데이터를 처리하는 메서드를 하나의 객체 안에 묶고, 외부에서 직접 접근하지 못하도록 은닉한다.
- 상속(Inheritance) : 한 클래스가 다른 클래스의 속성과 메서드를 물려받아 코드 재사용성을 높인다.
- 다형성(Polymorphism) : 같은 이름의 메서드가 상황에 따라 다른 동작을 하도록 한다.
- 추상화(Abstraction) : 복잡한 세부사항을 숨기고, 필요한 기능만 노출하여 단순화시킨다.
자바에서 객체지향 구현하기
클래스와 객체
- 클래스 : 객체를 생성하기 위한 설계도
- 객체 : 클래스의 인스턴스, 실제 메모리에 할당되어 동작한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Car {
// 속성 (필드)
private String brand;
private int speed;
// 생성자
public Car(String brand, int speed) {
this.brand = brand;
this.speed = speed;
}
// 메서드
public void drive() {
System.out.println(brand + "가 " + speed + "km/h로 주행 중입니다.");
}
}
// 객체 생성
Car myCar = new Car("Tesla", 100);
myCar.drive(); // 출력: Tesla 100km/h로 주행 중입니다.
캡슐화
- 데이터를 private 로 선언하고, getter 와 setter 메서드를 통해 접근을 제어한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Person {
private String name;
private int age;
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String name) {
this.name = name;
}
// Getter
public int getAge() {
return age;
}
// Setter
public void setAge(int age) {
if (age >= 0) {
this.age = age;
}
}
}
상속
- extends 키워드를 사용해 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Animal {
public void eat() {
System.out.println("동물이 밥을 먹습니다.");
}
}
public class Dog extends Animal {
public void bark() {
System.out.println("멍멍!");
}
}
// 사용
Dog dog = new Dog();
dog.eat(); // 출력: 동물이 밥을 먹습니다.
dog.bark(); // 출력: 멍멍!
다형성
- 메서드 오버로딩(Overloading)과 오버라이딩(Overriding)을 통해 구현된다.
- 오버로딩 : 같은 이름의 메서드에 매개변수를 다르게 정의
- 오버라이딩 : 부모 클래스의 메서드를 자식 클래스에서 재정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Animal {
public void makeSound() {
System.out.println("소리를 냅니다.");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("야옹!");
}
}
// 사용
Animal cat = new Cat();
cat.makeSound(); // 출력: 야옹!
추상화
- abstract 클래스나 interface 를 사용해 구현해야 할 메서드의 ‘규칙’을 정의한다.
1
2
3
4
5
6
7
8
9
10
public interface Flyable {
void fly();
}
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("새가 날아갑니다!");
}
}
SOLID : 좋은 객체지향 설계를 위한 5대 원칙
SOLID 는 객체지향의 4대 원칙을 기반으로, 더욱 유연하고 유지보수하기 쉬우며, 이해하기 편한 소프트웨어를 만들기 위한 5가지 설계 원칙이다.
SRP: 단일 책임 원칙(Single Responsibility Principle)
“하나의 클래스는 단 하나의 책임만 가져야 한다.”
클래스를 변경해야 할 이유가 오직 하나여야 한다는 뜻이다.
예를 들어, User 클래스가 사용자 정보 관리와 이메일 발송 기능을 모두 갖는 것은 SRP 위반이다.
UserService 와 EmailService 로 책임을 분리해야 한다.OCP: 개방-폐쇄 원칙(Open-Closed Principle)
“소프트웨어 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다.”
새로운 기능을 추가하더라도 기존 코드를 수정해서는 안 된다는 원칙이다
다형성과 추상화를 통해, 새로운 결제 수단을 추가할 때 PaymentService 코드를 바꾸는 대신 KakaoPay 클래스를 새로 추가하는 것만으로 기능이 확장되도록 설계해야 한다.LSP: 리스코프 치환 원칙(Liskov Substitution Principle)
“프로그램의 정확성을 깨뜨리지 않으면서, 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있어야 한다.”
상속 관계에서 자식 클래스가 부모의 기능을 오작동하게 만들면 안 된다는 뜻이다.
예를 들어, Bird 를 상속받은 Penguin 이 fly() 메서드를 제대로 수행하지 못한다면 LSP 위반이다.ISP: 인터페이스 분리 원칙(Interface Segregation Principle) “클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.”
너무 큰 인터페이스 하나보다, 역할에 맞게 잘게 쪼개진 여러 개의 인터페이스가 낫다는 원칙입니다.
예를 들어, SmartMachine 인터페이스에 print(), scan(), fax() 가 모두 있다면, scan 기능만 필요한 클라이언트도 print, fax 에 불필요하게 의존하게 된다. Printable, Scannable 등으로 인터페이스를 분리하는 것이 더 좋다.DIP: 의존관계 역전 원칙(Dependency Inversion Principle) “추상화에 의존해야지, 구체화에 의존하면 안 된다.”
세부적인 구현 클래스가 아니라, 추상적인 인터페이스나 상위 클래스에 의존해야 한다는 원칙이다. 스프링의 의존성 주입(DI)이 바로 이 원칙을 구현하는 대표적인 기술이다.
Controller 가 구체적인 MyService 클래스에 의존하는 게 아니라, Service 라는 인터페이스에 의존하게 만들어, 나중에 다른 구현체로 쉽게 교체할 수 있도록 설계해야 한다.