POJO(Plain Old Java Object)는 자바에서 특별한 기술이나 규약에 종속되지 않은 순수한 자바 객체입니다. POJO는 특정 프레임워크나 라이브러리에 의존하지 않고, 자바의 기본 문법과 객체 지향 원칙을 따릅니다. 각 단어를 자세히 분석해 보면:
Plain: 특별한 기능이나 제약 없이 "단순한" 객체라는 뜻입니다. 복잡한 설정이나 규칙 없이 작성된 자바 클래스입니다.
Old: 과거 방식이라는 의미를 넘어서, 기존에 사용되던 단순한 자바 객체를 지칭하며, 특정 기술이나 프레임워크의 "새로운" 방식에 종속되지 않는다는 의미로 사용됩니다.
Java: 자바 프로그래밍 언어로 작성된 객체입니다. 자바 표준 문법을 따르는 클래스입니다.
Object: 자바 객체입니다. 이는 자바의 모든 클래스 인스턴스를 의미하며, 자바에서 모든 객체는 기본적으로 Object 클래스를 상속받습니다.
POJO는 자바 프로그래밍에서 객체지향 개념을 기반으로, 불필요한 복잡성을 배제하고 간단한 구조로 작성된 객체를 뜻합니다. 특정 라이브러리나 프레임워크의 특별한 규칙을 따르지 않고, 단순히 자바 문법에 맞춰 작성됩니다.
POJO의 가장 중요한 특징 중 하나는 프레임워크에 종속되지 않음입니다. 이는 특정 기술에 구애받지 않고 자유롭게 작성된 객체라는 뜻입니다. 이렇게 작성된 객체는 특정 환경에 종속되지 않고 재사용이 쉽습니다.
예시: POJO 예시로 간단한 Person 클래스
public class Person {
private String name;
private int age;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
위 Person 클래스는 어떤 프레임워크에도 종속되지 않은 순수한 자바 객체입니다. 이는 자바의 기본 문법과 객체 지향 원칙만을 따르고 있으며, 특정 라이브러리나 기술의 영향을 받지 않습니다.
POJO는 자바의 객체지향 프로그래밍(Object-Oriented Programming, OOP) 원칙을 따릅니다. 객체지향 프로그래밍의 주요 원칙으로는 캡슐화, 상속, 다형성, 추상화가 있습니다. POJO는 이러한 원칙을 준수하여 설계되며, 이를 통해 코드의 유지보수성, 재사용성이 높아집니다.
캡슐화는 객체의 속성(데이터)을 외부에서 직접 접근하지 못하도록 숨기고, 필요한 경우 메서드를 통해 접근할 수 있도록 하는 개념입니다. 자바에서는 private 키워드를 사용해 필드를 감추고, getter와 setter 메서드를 통해 외부에서 데이터를 접근할 수 있게 합니다.
예시:
public class BankAccount {
private double balance;
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
public void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
}
}
}
BankAccount 클래스에서 balance 필드는 외부에서 직접 접근할 수 없고, deposit과 withdraw 메서드를 통해서만 접근할 수 있습니다. 이를 통해 데이터 무결성을 보호할 수 있습니다.
상속은 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 작성하는 것을 의미합니다. 상속을 통해 코드 재사용성을 높이고, 기존 클래스를 확장할 수 있습니다.
예시:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Employee extends Person {
private String position;
public Employee(String name, int age, String position) {
super(name, age);
this.position = position;
}
public String getPosition() {
return position;
}
}
Employee 클래스는 Person 클래스를 상속받아 name과 age 필드를 재사용하고, position 필드를 추가로 정의합니다. 이를 통해 중복 코드를 줄이고, 기능을 확장할 수 있습니다.
다형성은 하나의 객체가 여러 형태를 가질 수 있는 성질입니다. 자바에서는 메서드 오버로딩과 오버라이딩을 통해 다형성을 구현할 수 있습니다. 메서드 오버로딩은 같은 이름의 메서드를 여러 형태로 정의하는 것을 의미하며, 오버라이딩은 상위 클래스의 메서드를 하위 클래스에서 재정의하는 것을 뜻합니다.
메서드 오버라이딩 예시:
public class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
public class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
Animal 클래스는 sound() 메서드를 정의하고, Dog와 Cat 클래스는 이를 오버라이딩하여 각각 다른 소리를 내도록 합니다. 같은 sound() 메서드를 호출해도 클래스에 따라 다른 동작을 수행하는 다형성을 보여줍니다.
추상화는 복잡한 내부 구현을 숨기고 필요한 인터페이스만 외부에 공개하는 원칙입니다. 자바에서는 추상 클래스나 인터페이스를 통해 추상화를 구현할 수 있습니다. 추상 클래스는 하나 이상의 추상 메서드를 포함할 수 있으며, 구체적인 동작은 하위 클래스에서 정의됩니다.
예시:
public abstract class Vehicle {
public abstract void move();
}
public class Car extends Vehicle {
@Override
public void move() {
System.out.println("The car drives");
}
}
public class Bike extends Vehicle {
@Override
public void move() {
System.out.println("The bike pedals");
}
}
Vehicle 클래스는 추상 클래스이며, Car와 Bike 클래스는 각각 구체적인 move() 메서드를 구현합니다. 추상화를 통해 공통적인 인터페이스를 정의하고, 각 클래스는 세부 구현을 담당합니다.
직렬화(Serialization)는 객체를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하는 과정입니다. 직렬화된 객체는 역직렬화(Deserialization)를 통해 다시 객체로 복원될 수 있습니다. 자바에서는 직렬화를 위해 Serializable 인터페이스를 구현해야 합니다.
직렬화 예시:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String email;
public User(String username, String email) {
this.username = username;
this.email = email;
}
public String getUsername() {
return username;
}
public String getEmail() {
return email;
}
}
위 User 클래스는 Serializable 인터페이스를 구현하여 직렬화가 가능해졌습니다. 직렬화는 객체의 상태를 파일에 저장하거나 다른 시스템으로 전송할 때 사용됩니다.
의존성 주입은 객체 간의 의존성을 외부에서 주입하여 객체 간 결합도를 낮추고, 유지보수를 쉽게 하기 위한 설계 패턴입니다. 이는 객체가 자신이 의존하는 객체를 직접 생성하지 않고, 외부에서 받아서 사용하는 방식입니다. 의존성 주입은 POJO와 프레임워크가 느슨하게 결합되도록 도와줍니다.
의존성 주입의 주요 방식
생성자 주입 (Constructor Injection): 생성자를 통해 의존성을 주입합니다.
예시:
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
세터 주입 (Setter Injection): 세터 메서드를 통해 의존성을 주입합니다.
예시:
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
필드 주입 (Field Injection): 필드에 직접 의존성을 주입하는 방식입니다. 스프링 프레임워크에서는 @Autowired 어노테이션을 사용해 필드 주입을 할 수 있습니다.
예시:
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
의존성 주입을 사용하면 클래스 간 결합도를 낮추고, 재사용성과 유지보수성을 높일 수 있습니다. POJO는 의존성 주입을 통해 독립적인 객체로 유지되면서도 프레임워크와의 통합을 용이하게 할 수 있습니다.
결합도는 객체 간의 의존성을 의미합니다. 결합도가 높으면, 객체들이 서로 강하게 의존하게 되어 변경이 발생하면 다른 객체들도 함께 수정해야 하는 불편함이 생깁니다. 낮은 결합도를 유지하는 것이 좋습니다.
낮은 결합도 예시:
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void processOrder() {
paymentService.processPayment();
}
}
OrderService는 PaymentService와 느슨한 결합을 유지하고 있으며, 다른 결제 시스템으로 변경할 때도 OrderService를 수정할 필요가 적습니다.
응집도는 객체 내부의 구성 요소들이 서로 얼마나 밀접하게 관련되어 있는지를 나타냅니다. 높은 응집도는 하나의 클래스가 하나의 책임을 갖고, 그 내부의 메서드와 필드가 긴밀히 연결되어 있다는 것을 의미합니다.
높은 응집도 예시:
public class Customer {
private String name;
private String email;
public Customer(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
}
위 Customer 클래스는 고객 정보만을 다루고 있으며, 내부적으로 응집된 책임을 갖고 있어 응집도가 높습니다.
POJO는 특정 프레임워크에 종속되지 않지만, 다양한 프레임워크와 함께 사용될 수 있습니다. 스프링 프레임워크는 POJO를 중심으로 설계된 대표적인 프레임워크입니다. 스프링은 POJO 기반의 비즈니스 로직을 쉽게 통합하고 관리할 수 있게 해줍니다. 스프링은 POJO와 프레임워크를 느슨하게 결합시키기 위해 의존성 주입, AOP(Aspect-Oriented Programming) 등 다양한 기법을 제공합니다.
Spring과 POJO의 통합 예시:
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
}
위 코드에서 UserService는 스프링의 @Service 어노테이션을 사용하지만, 여전히 POJO의 성격을 유지하고 있습니다. 이는 프레임워크에 종속되지 않으며, 스프링의 의존성 주입을 통해 필요한 객체를 주입받아 동작합니다.
POJO는 자바 객체지향 프로그래밍의 기본 개념을 기반으로 하며, 특정 프레임워크나 기술에 종속되지 않은 순수한 자바 객체입니다. POJO의 주요 특징인 프레임워크 독립성, 객체지향 원칙 준수, 테스트 용이성, 재사용성은 자바 애플리케이션 개발에서 중요한 요소입니다. 객체지향 프로그래밍의 원칙(캡슐화, 상속, 다형성, 추상화)과 의존성 주입(DI), 결합도와 응집도를 관리하여 유연한 코드 구조를 만들 수 있습니다.
POJO는 스프링과 같은 프레임워크와 함께 사용될 때도 느슨한 결합을 유지하여 코드의 유지보수성을 높이고, 애플리케이션의 유연성과 확장성을 보장합니다.