자바의 캡슐화 - Private, Protected, Default, Public

2024. 1. 10. 18:29Java

[ 작성 이유 ]

 자바에 관한 책들을 보면 자바의 객체지향적 특징으로 캡슐화, 상속, 추상화, 다형성을 언급하는 곳이 많았다. 그런데 책의 내용을 읽으면 나머지는 어느정도 이해가 됐지만 캡슐화는 접근자에 관한 이야기와 범위에 관한 내용 외에는 머리에 남는 내용이 없었다. 하지만 뭔지는 몰라도 'public 막쓰면 안돼요.', 'setter 쓰지 마세요.', 이런 말들은 강의를 듣거나 남의 코드 리뷰를 보면 많이 나오는 피드백이다. 이 이유들에 관해서 나름대로의 생각을 적어보려고 한다. 아직 디자인 패턴 및 클린코드에 대해서 배우지는 않았지만 Spring을 공부하면서 혹은 Java를 공부하면서 든 생각이니 착각하는게 있다면 바로 잡을 수 있게 도와주었으면 한다.

[ 캡슐화 ]

 캡슐화의 핵심은 관련이 있는 변수와 함수를 하나의 클래스로 묶고 외부에서 쉽게 접근하지 못하도록 은닉하 는 것이라고 한다. 객체에 직접적인 접근을 막고 객체가 제공하는 필드와 메서드를 통해 접근이 가능하다. 그러면 정보변경을 못하게 하고 유지보수나 확장시 오류를 최소화할 수 있게 해준다.

 

 이를 위해 제공되는 방식은 접근 제어자이다. 클래스 접근 자에는 2가지가 있고 메서드 접근 제어자에는 4가지가 있다.

 

클래스 접근 생성자

default: 동일 패키지 클래스만 인스턴스를 생성가능하다.

public: 다른 패키지에서 인스턴스(객체) 생성이 가능하다.

 

메서드 접근 제어자

private: 동일한 클래스 안에서만 접근이 가능하다. this를 사용하여 자기 자신 객체를 참조가 가능하다.

default: 접근제어자 없는 형태, 동일 패키지 내에서만 사용이 가능하다.

protected: 동일한 패키지 안에서만 사용 가능

public: 모든 객체에서 접근 가능

 

 보통 대부분은 이렇게만 설명이 되어있다. 이 설명을 보면 뭘 어떻게 써야할지 전혀 감이 오지 않는다. 그래서 4가지 종류를 한번 뜯어서 이해를 해보자.

[ Private 접근자 ]

 private 접근자를 사용해야 하는 경우들에 대해서 예시를 몇 가지 생각을 해보자. 첫 번째로 떠오른 예시는 싱글톤 패턴으로 자바에서 스프링 컨테이너를 구현하는 경우가 있을 것 같다. 싱글톤 패턴을 사용할 때에 중요한 점은 미리 만들어놓은 객체를 재활용함으로써 새로운 메모리를 할당하지 않는 것이었다. 그 방법을 시행하기 위해서 싱글톤 패턴에서 주로 사용하는 방법은 Constructor를 private으로 설정하고 클래스 내부에서 private static final로 된 싱글톤 객체를 생성하게 한 뒤 참조용 메서드로 getInstance() 같은 걸 두는 방식이었다. 

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }

    private SingletonService(){}
}

 

 위의 코드처럼 말이다. 이러면 해당 객체는 프로그램이 작성하는 동안 단 1개임을 확정 지을 수 있고 더 이상 다른 곳에서 마음대로 바꾸지 못한다. 

 

 그리고 private을 또 사용할만한 공간은 멀티쓰레드를 사용하는 환경이나 그런 곳에서 마음대로 객체 인스턴스 내부의 값들을 변경하지 못하게 할 때도 사용한다. 이는 쓰레드 프로그래밍을 공부해보면 알 수 있다. 중요한 변수를 아무리 임계영역으로 가져다 놓아도 public으로 접근 제어자를 가져다 놓으면 해당 변수를 사용하는 로직이 많아질수록 멀티쓰레드 환경에서 변수를 보호하기 힘들어진다.

 

 말 그대로 프로그램의 보안과 관련된 내용으로 협업 과정 중 상상하지 못한 곳에서 다른 프로그래머들이 인스턴스.변수명 형태로 값을 마음대로 바꾸는 일을 막아주는 역할을 함으로써 프로그램의 유지보수를 도와준다. 이 점에서는 위에서 캡슐화에 대한 설명이 잘 나와있다고 생각을 한다. 이런 점에서 끝나면 좋겠지만 private 변수를 접근하긴 해야하니까 Getter와 Setter를 Java에서는 public으로 만듦으로써 private 변수를 마치 public 변수처럼 사용할 수 있게 해준다. 이 부분은 다음 편에서 Getter와 Setter에 대해서 알아보면서 설명을 하려고 한다.

[ Protected 접근자 ]

 가장 모호한 접근 제어자라고 생각을 한다. 사실 이 내용을 정리하려다가 적게 된 건데 Protected 접근 제어자에 대한 내 생각은 어디에 사용하는지 가장 명확하지 않고 와닿지 않는 느낌이다. MVC 패턴 공부를 하면서 서블릿 기반 Rest API를 공부하다가 처음 protected 접근 제어자를 쓰는 메서드를 만났다.

 

 아마 요즘은 이렇게 코드를 짜는 사람이 없겠지만 인프런에서 영한님의 spring 강의에서 가져온 코드를 가져와봤다. 해당 내용은 HttpServlet 클래스를 상속 받아서 API를 설계하고 new-form.jsp 템플릿을 메시지 응답 결과로 내주는 함수이다.

@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        // Dispatcher.forward(); 다른 서블릿이나 JSP로 이동할 수 있는 기능으로 서버 내부에서 호출이 다시 일어난다.
        // WEB-INF 해당 경로에 있으면 직접 접근이 불가능하고 반드시 서버 내부에서 컨트롤러를 통해 JSP를 호출해야지 사용가능
        dispatcher.forward(request,response);
    }
}

 

 해당 변수를 보면 protected라고 되어있는데, 처음에는 protected를 처음봐서 '오... 신기하네'하고 넘겼었다. 나중에 다시 캡슐화를 정리할 겸 보면서 검색을 해보니, "protected는 잠재적으로 자식 클래스가 Override해서 바꾸어야할 경우를 고려한 Modifier"라는 설명이 있었다. 처음에 이 말을 들었을 때, 아니 그럼 abstract를 쓰면 되는게 아닌가?라는 생각이 들었다.

 

그래서 잠깐 고민의 결과 다시 한번 작성을 해보았다. HttpServlet에 대해서 그냥 extends한 뒤에 함수를 작성하지 않았을 때 인텔리제이 IDE에서 빨간 줄을 보여야하는데 빨간 줄을 보이지 않았다. 강의를 공부할 때에는 인지하지 못했지만 protected는 경고메시지를 보내지는 않는다. service를 딱 작성하려고 하면 service 내부에는 super.service(req, res); 이렇게만 뜰 것이다. HttpServlet.java에 들어가서 내부를 살펴보았다.

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;

    if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) {
        throw new ServletException("non-HTTP request or response");
    }

    request = (HttpServletRequest) req;
    response = (HttpServletResponse) res;

    service(request, response);
}

 

 기본적으로는 service에서 req와 res가 인스턴스가 맞는지 확인하고 내부에서 다른 service를 호출하는 것을 구현이 되어있다. 이걸 override해서 request나 response에 적절한 처리를 하거나 필요한 값들을 가져오고 다시 HTTP Method별 요청이 담긴 service를 호출할 수 있게 해준다. 

 

 솔직하게 어떻게 override됐을 때 HTTP Method 별 요청이 담긴 service가 호출되는지는 정확하게 모르지만 이를 통해서 알게 된 것은 protected의 사용 방법과 abstract의 차이였다. 이 부분은 이후에 생각이 나면 작성을 하도록 하겠다.

 

 abstract는 "난 미래는 잘 모르겠고 반환 타입이랑 결과는 내가 정해놨고 확장하더라도 이게 필요하니까 쓰는 사람이 작성은 해놔야지 돌아갈거야"라는 거고 protected는 "나도 미래는 잘 모르겠지만 이 정도여도 난 될 거라고 생각을 했지만 확신은 안드네? 그 메서드에서 들어오는 값 결과로 추가적인 뭔가를 하고 싶으면 프로그래머인 너가 protected를 봤으면 부족한 부분을 한 번 다시 고민해봐."라는 의미로 난 해석을 했다.

 

 그리고 public과 default가 남았는데, public은 private과 protected의 반대라고 생각을 한다. 딱히 상태가 유지되지도 않고 그 자체로 완벽해서 더 이상 수정이 필요없는 메서드 혹은 변수에 사용되는 것이라고 고민의 결과를 내렸다.

 

 또한 Default는 정확하게는 모르겠지만 패키지 외부에서는 참조가 불가능하니 라이브러리 같은데서 많이 사용하는 것 같다. 외부에서 라이브러리 함수를 보호하기 위한 조치인 것 같다. 

 

** 추가 내용: 2024-02-13 추가 **

 JPA에서 객체와 테이블 사이를 매핑해주는 Entity를 설계할 때에 Entity 자체에 어느정도 비즈니스 로직과 관련이 있는 코드들을 생성할 수도 있다. 왜냐하면 JPA의 가장 큰 장점은 영속성 컨텍스트가 dirty checking을 해주는 것이기 때문이다. 이런 설계에서 조심해야 할 점이 있다.

 

 Spring Data JPA를 사용하기 위해서는 기본 NoArgsConstructor를 만들어놔야한다. 그런데 문제는 이걸 JPA가 데이터 변경 감지를 위해서는 public이나 protected로 열어놔야 한다. 그런데 public으로 놔두면 다른 협업하는 누군가가 Entity에 어느정도 Dto관련 로직을 채워넣으라고 그렇게 만들어놨건만 채워넣으라는건가? 하고 new Entity()이렇게 해서 채워버릴 수도 있다. 이럴 때에 Spring에서는 protected까지 허용해주니, Entity만 따로 패키지 내부에 넣어두고 protected로 만들어버리면 new 생성자로 만들 때 에러가 발생하므로 이런 현상을 방지할 수 있다. 이럴 때도 protected를 사용할 수 있다.

 [ 참고자료 ]

https://radait.tistory.com/5

https://blog.naver.com/2feelus/220576845725

 

 

'Java' 카테고리의 다른 글

static initializer block와 initializer block  (0) 2024.01.19
static 키워드  (0) 2024.01.17
Java의 메모리 구조  (0) 2023.12.28
JVM, JRE, JDK의 차이가 무엇일까?  (0) 2023.12.28
Primitive Type과 Wrapper Class  (2) 2023.12.23