<객체지향 프로그래밍의 이해>
Java는 객체지향 프로그래밍 언어로 널리 알려져 있다. 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 현실 세계의 객체를 소프트웨어로 모델링하여 프로그램을 구성하는 패러다임이다. Java는 이러한 객체지향 개념을 언어 차원에서 지원하여 코드의 재사용성, 유지보수성, 확장성을 높인다.
많은 개발자가 "Java는 객체지향 언어다"라는 말을 듣지만, 실제 프로젝트 코드를 통해 구체적으로 이해하는 경우는 드물다. 객체지향의 핵심 원칙을 이론으로만 접하면 실무에서 어떻게 활용되는지 파악하기 어렵다. 이에 따라 본 포스팅에서는 실제 작성한 프로젝트 코드를 바탕으로 Java의 객체지향적 특징을 구체적으로 살펴본다.
<객체지향의 4대 핵심 원칙>
객체지향 프로그래밍은 네 가지 핵심 원칙으로 구성된다. 이 원칙들은 소프트웨어 설계의 기본이 되며, Java는 이를 언어 차원에서 완벽하게 지원한다.
- 캡슐화 (Encapsulation)
캡슐화는 데이터와 그 데이터를 다루는 메서드를 하나의 단위로 묶고, 외부로부터 데이터를 보호하는 메커니즘이다. Java에서는 접근 제한자를 통해 이를 구현한다.
- 상속 (Inheritance)
상속은 기존 클래스의 필드와 메서드를 새로운 클래스가 물려받아 재사용할 수 있게 하는 기능이다. 이를 통해 코드 중복을 줄이고 계층적 관계를 표현한다.
- 다형성 (Polymorphism)
다형성은 동일한 타입의 참조 변수로 서로 다른 객체를 참조할 수 있는 능력이다. 이를 통해 유연하고 확장 가능한 코드를 작성할 수 있다.
- 추상화 (Abstraction)
추상화는 복잡한 시스템에서 핵심적인 개념만 추출하여 간단하게 표현하는 것이다. 불필요한 세부 사항을 숨기고 중요한 기능만 노출한다.
<캡슐화: 데이터 보호와 정보 은닉>
캡슐화는 객체의 내부 상태를 외부로부터 보호하고, 정해진 방법을 통해서만 접근하도록 제한하는 원칙이다. 이를 통해 데이터의 무결성을 보장하고 객체의 독립성을 유지한다.
- 기본적인 캡슐화 구현
캡슐화의 가장 기본적인 형태는 필드를 private으로 선언하고 public 메서드를 통해 접근하는 방식이다. 다음은 Person 클래스에서 캡슐화를 구현한 예시이다.
public class Person {
private String name;
private int age;
private String gender;
public Person() {
super();
}
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getGender() {
return gender;
}
public String toString() {
return name + " " + age + " " + gender;
}
}
위 코드에서 name, age, gender 필드는 모두 private으로 선언되어 외부에서 직접 접근할 수 없다. 대신 getName(), getAge(), getGender() 같은 public 메서드를 통해서만 데이터에 접근한다. 이러한 방식으로 외부에서 잘못된 값을 직접 대입하는 것을 방지한다.
- 데이터 검증을 포함한 캡슐화
캡슐화의 진정한 가치는 단순히 필드를 숨기는 것이 아니라, 데이터의 유효성을 검증하는 로직을 포함할 수 있다는 점에 있다. Book 클래스의 예시를 살펴본다.
public class Book {
private String title;
private String genre;
private String author;
private int maxPage;
public Book() {
super();
}
public Book(String title, String genre, String author, int maxPage) {
super();
this.title = title;
this.genre = genre;
this.author = author;
this.maxPage = maxPage;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getGenre() {
return genre;
}
public void setGenre(String genre) {
this.genre = genre;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getMaxPage() {
return maxPage;
}
public void setMaxPage(int maxPage) {
if(maxPage < 1) {
this.maxPage = 1;
return;
}
this.maxPage = maxPage;
}
}
setMaxPage() 메서드는 페이지 수가 1보다 작은 경우 자동으로 1로 설정하는 검증 로직을 포함한다. 만약 필드를 public으로 선언하여 외부에서 직접 접근했다면 이러한 검증이 불가능했을 것이다. 캡슐화를 통해 데이터의 유효성을 보장하고, 객체의 일관성을 유지할 수 있다.
- 캡슐화의 실무적 이점
캡슐화는 다음과 같은 실무적 이점을 제공한다. 첫째, 데이터의 무결성을 보장하여 잘못된 값의 입력을 방지한다. 둘째, 객체의 내부 구현을 변경해도 외부 코드에 영향을 주지 않는다. 셋째, 코드의 유지보수성이 향상되어 수정이 필요한 경우 해당 클래스 내부만 변경하면 된다.
<상속: 코드 재사용과 계층적 구조>
상속은 기존 클래스의 속성과 동작을 새로운 클래스가 물려받아 재사용하는 메커니즘이다. Java에서는 extends 키워드를 사용하여 상속을 구현한다. 상속을 통해 코드 중복을 줄이고, 클래스 간의 계층적 관계를 명확히 표현할 수 있다.
- Servlet에서의 상속 활용
웹 애플리케이션 개발에서 Servlet은 상속의 대표적인 활용 사례이다. 다음은 HttpServlet을 상속받은 컨트롤러 클래스의 예시이다.
@WebServlet("/list.bo")
public class ListController extends HttpServlet {
private static final long serialVersionUID = 1L;
public ListController() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int currentPage = request.getParameter("cpage") != null ?
Integer.parseInt(request.getParameter("cpage")) : 1;
int listCount = new BoardService().selectAllBoardCount();
PageInfo pi = new PageInfo(currentPage, listCount, 5, 5);
ArrayList<Board> list = new BoardService().selectAllBoard(pi);
request.setAttribute("list", list);
request.setAttribute("pi", pi);
request.getRequestDispatcher("views/board/listView.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
ListController는 HttpServlet을 상속받아 웹 요청 처리에 필요한 모든 기능을 자동으로 사용할 수 있다. doGet(), doPost() 메서드를 오버라이딩하여 자신만의 로직을 구현하되, HttpServlet이 제공하는 나머지 기능은 그대로 활용한다. 이를 통해 개발자는 웹 요청 처리의 복잡한 내부 메커니즘을 신경 쓰지 않고 비즈니스 로직에만 집중할 수 있다.
- 일반 클래스에서의 상속 관계
상속은 Servlet뿐만 아니라 일반적인 클래스 설계에서도 널리 활용된다. Man 클래스와 이를 상속받는 BusinessMan 클래스의 예시를 살펴본다.
public class Man {
private String name;
protected Man() {
super();
System.out.println("Man의 기본생성자");
}
protected Man(String name) {
super();
this.name = name;
System.out.println("Man에 name이 포함된 생성자");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void tellYourName() {
System.out.println("My name is " + name);
}
}
public class BusinessMan extends Man {
private String company;
private String position;
protected BusinessMan(String name, String company, String position) {
super(name);
this.company = company;
this.position = position;
}
public void tellYourInfo() {
System.out.println("My company is "+ company);
System.out.println("My Position is "+ position);
super.tellYourName();
}
}
BusinessMan은 Man을 상속받아 name 필드와 tellYourName() 메서드를 자동으로 사용할 수 있다. super(name)을 통해 부모 클래스의 생성자를 호출하여 name 필드를 초기화하고, super.tellYourName()을 통해 부모 클래스의 메서드를 재사용한다.
- 상속의 핵심 장점
상속은 코드 재사용성을 극대화하여 동일한 코드를 반복해서 작성할 필요가 없다. 부모 클래스의 기능을 수정하면 이를 상속받은 모든 자식 클래스에 자동으로 반영되어 유지보수가 용이하다. 또한 클래스 간의 계층적 관계를 명확히 표현하여 코드의 구조를 이해하기 쉽게 만든다.
<다형성: 유연한 코드 구조의 핵심>
다형성은 하나의 타입으로 여러 종류의 객체를 참조할 수 있는 능력이다. Java에서는 인터페이스와 상속을 통해 다형성을 구현하며, 이를 통해 코드의 유연성과 확장성을 크게 높일 수 있다.
- 인터페이스를 통한 다형성
인터페이스는 다형성을 구현하는 가장 대표적인 방법이다. 다음은 BoardService 인터페이스의 예시이다.
public interface BoardService {
// 카테고리 관련
List<Category> getCategories();
// 게시판 관련
Map<String, Object> getBoardList(int currentPage);
Map<String, Object> getSearchBoardList(int currentPage, String condition, String keyword);
int insertBoard(Board board, MultipartFile file);
Map<String,Object> getBoardByIdWithCount(int boardNo);
Map<String,Object> getBoardById(int boardNo);
int updateBoard(Board board, MultipartFile file, Integer originFileNo);
int deleteBoard(int boardNo);
// 댓글 관련
int insertReply(Reply reply);
List<Reply> getReplyListByBoardNo(int boardNo);
int removeReply(int replyNo);
// 썸네일 게시판 관련
List<Board> getThumbnailList();
Map<String, Object> getThumbnailBoardDetail(int boardNo);
int insertThumbnailBoard(Board board, List<MultipartFile> attachmentList);
}
BoardService 인터페이스는 게시판 관련 기능의 표준을 정의한다. 이 인터페이스를 구현하는 여러 클래스가 각각 다른 방식으로 동일한 메서드를 구현할 수 있다. Controller에서는 BoardService 타입으로 참조하여 실제 구현체가 무엇인지 신경 쓰지 않고 사용한다.
- Service 클래스의 구현
인터페이스를 구현한 구체적인 클래스는 다음과 같다.
public class BoardService {
private BoardDao boardDao = new BoardDao();
public int selectAllBoardCount() {
SqlSession sqlSession = Template.getSqlSession();
int listCount = boardDao.selectAllBoardCount(sqlSession);
sqlSession.close();
return listCount;
}
public int selectSearchBoardCount(HashMap<String, String> searchMap){
SqlSession sqlSession = Template.getSqlSession();
int listCount = boardDao.selectAllBoardCount(sqlSession, searchMap);
sqlSession.close();
return listCount;
}
public ArrayList<Board> selectSearchBoard(HashMap<String, String> searchMap, PageInfo pi){
SqlSession sqlSession = Template.getSqlSession();
ArrayList<Board> list = boardDao.selectAllBoard(sqlSession, searchMap, pi);
sqlSession.close();
return list;
}
public ArrayList<Board> selectAllBoard(PageInfo pi){
SqlSession sqlSession = Template.getSqlSession();
ArrayList<Board> list = boardDao.selectAllBoard(sqlSession, pi);
sqlSession.close();
return list;
}
}
Controller에서 BoardService를 사용할 때 내부 구현이 어떻게 되어 있는지 알 필요가 없다. 나중에 다른 방식으로 구현한 Service로 교체해도 Controller 코드는 변경할 필요가 없다.
- 다형성이 제공하는 이점
다형성은 코드의 유연성을 극대화하여 새로운 구현체를 추가하더라도 기존 코드를 수정할 필요가 없다. 인터페이스를 기준으로 프로그래밍하면 구현체 교체가 자유로워 확장성이 높아진다. 또한 테스트 시에도 Mock 객체를 쉽게 주입하여 단위 테스트를 작성할 수 있다.
<추상화: 복잡성을 숨기고 핵심만 표현>
추상화는 복잡한 시스템에서 핵심적인 개념만 추출하여 간단하게 표현하는 것이다. 불필요한 세부 사항을 숨기고 중요한 기능만 노출함으로써 코드의 가독성과 사용성을 높인다.
- Servlet의 추상화
Servlet을 사용할 때 개발자는 웹 서버의 복잡한 내부 동작 방식을 알 필요가 없다. 단지 doGet(), doPost() 메서드만 구현하면 웹 요청을 처리할 수 있다.
@WebServlet("/basic.do")
public class ElServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public ElServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 요청 처리 로직
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
HttpServlet은 HTTP 프로토콜의 복잡한 처리 과정을 내부에 숨기고, 개발자에게는 doGet(), doPost() 같은 간단한 메서드만 제공한다. 개발자는 TCP/IP 통신, HTTP 헤더 파싱, 요청 라우팅 등의 저수준 작업을 몰라도 웹 애플리케이션을 개발할 수 있다.
- 추상화의 핵심 가치
추상화는 복잡한 내부 구현을 숨겨 사용자가 쉽게 이해하고 사용할 수 있게 한다. 세부 사항이 변경되어도 추상화된 인터페이스는 유지되므로 코드의 안정성이 높아진다. 개발자는 핵심 로직에만 집중할 수 있어 생산성이 향상된다.
<MVC 아키텍처: 객체지향 원칙의 종합>
프로젝트에서는 앞서 설명한 객체지향 원칙들을 조합하여 MVC(Model-View-Controller) 패턴을 구현한다. MVC는 애플리케이션을 세 개의 논리적 계층으로 분리하여 각 계층의 독립성을 보장한다.
- Controller: 요청 처리와 흐름 제어
Controller는 사용자의 요청을 받아 적절한 Service를 호출하고, 결과를 View에 전달하는 역할을 수행한다.
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
int currentPage = request.getParameter("cpage") != null ?
Integer.parseInt(request.getParameter("cpage")) : 1;
int listCount = new BoardService().selectAllBoardCount();
PageInfo pi = new PageInfo(currentPage, listCount, 5, 5);
ArrayList<Board> list = new BoardService().selectAllBoard(pi);
request.setAttribute("list", list);
request.setAttribute("pi", pi);
request.getRequestDispatcher("views/board/listView.jsp").forward(request, response);
}
Controller는 비즈니스 로직을 직접 처리하지 않고 Service 계층에 위임한다. 이를 통해 요청 처리 로직과 비즈니스 로직을 명확히 분리한다.
- Service: 비즈니스 로직 처리
Service 계층은 애플리케이션의 핵심 비즈니스 로직을 담당한다. DAO를 호출하여 데이터에 접근하고, 트랜잭션을 관리한다.
- Model: 데이터 표현
Model은 데이터를 담는 VO(Value Object) 클래스로, 데이터베이스 테이블과 매핑된다. 캡슐화 원칙에 따라 필드를 private으로 선언하고 Getter/Setter를 제공한다.
- MVC 패턴의 장점
MVC 패턴은 각 계층의 역할을 명확히 분리하여 유지보수성을 향상시킨다. Controller가 변경되어도 Service나 Model에는 영향을 주지 않는다. 각 계층을 독립적으로 개발하고 테스트할 수 있어 협업 효율성이 높아진다. Service나 Model을 다른 Controller에서도 재사용할 수 있어 코드 재사용성이 극대화된다.
<객체지향 설계의 장점과 가치>
- 코드 재사용성과 생산성 향상
한 번 작성한 클래스를 여러 곳에서 재사용할 수 있어 개발 시간이 단축된다. 상속을 통해 기존 코드를 확장하여 새로운 기능을 빠르게 추가할 수 있다. 검증된 코드를 재사용하므로 버그 발생 가능성이 줄어든다.
- 유지보수성과 안정성 확보
관련된 데이터와 메서드를 하나의 클래스로 묶어 관리하므로 수정이 필요한 부분을 쉽게 찾을 수 있다. 캡슐화를 통해 내부 구현을 변경해도 외부 코드에 영향을 주지 않는다. 추상화를 통해 세부 구현이 변경되어도 인터페이스는 유지되어 코드의 안정성이 보장된다.
- 확장성과 유연성 제공
새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있다. 인터페이스를 통해 표준을 정의하고 다양한 구현체를 제공할 수 있다. 다형성을 활용하여 런타임에 객체를 교체하거나 확장할 수 있다.
- 협업 효율성 증대
각 개발자가 담당하는 클래스를 명확히 분리하여 병렬 작업이 가능하다. 인터페이스를 먼저 정의하면 구현이 완료되지 않아도 다른 개발자가 작업을 진행할 수 있다. 클래스 단위로 작업을 분담하므로 팀 협업이 효율적으로 이루어진다.
<정리>
Java가 객체지향 언어로 불리는 이유는 단순히 클래스와 객체를 사용하기 때문이 아니다. Java는 캡슐화, 상속, 다형성, 추상화라는 객체지향의 4대 핵심 원칙을 언어 차원에서 완벽하게 지원한다.
실제 프로젝트에서 캡슐화를 통해 데이터를 보호하고, 상속을 통해 코드를 재사용하며, 다형성을 통해 유연한 구조를 만들고, 추상화를 통해 복잡성을 관리한다. 이러한 원칙들이 조합되어 MVC 아키텍처 같은 견고한 설계 패턴이 탄생한다.
객체지향 프로그래밍은 코드의 재사용성, 유지보수성, 확장성, 협업 효율성을 동시에 향상시킨다. 이론으로만 접근하면 추상적으로 느껴질 수 있지만, 실제 코드를 통해 살펴보면 객체지향 원칙이 얼마나 실용적이고 강력한지 이해할 수 있다.
프로젝트를 진행하면서 객체지향 원칙을 의식적으로 적용하는 것이 중요하다. 단순히 동작하는 코드를 작성하는 것을 넘어, 왜 이렇게 설계했는지, 어떤 원칙을 따르고 있는지 항상 되돌아보는 자세가 필요하다. 객체지향적 사고방식을 체득하면 더 나은 소프트웨어를 설계하고 구현할 수 있다.
'Java' 카테고리의 다른 글
| Java_18) Runtime Data Area - Java 메모리 영역 (0) | 2025.11.26 |
|---|---|
| Java_17) Java Virtual Machine (0) | 2025.11.24 |
| Java_15)Java Object 클래스와 다형성 완벽 정리 (0) | 2025.11.13 |
| Java_14) Garbage Collection (1) | 2025.11.11 |
| Java_13) 오버로딩과 오버라이딩 (0) | 2025.11.10 |