자바에서 접근 제어자 사용은 어떻게 해야할까?

bjk1649·2023년 3월 19일
0

Java

목록 보기
3/3
post-thumbnail

제어자는 클래스와 클래스 안의 멤버 선언 시 사용합니다.

저는 제어자가 함께 일하는 개발자에게 코드 작성의 의도를 전달하여 로직을 보호하기 위해 사용된다고 생각합니다.

제어자는 크게 접근 제어자기타 제어자로 나누어집니다.

접근 제어자는 선언 한 번에 한 개만 사용할 수 있지만, 기타 제어자는 상황에 따라 두 개 이상을 조합해서 사용할 수도 있습니다.

접근 제어자에는 다음과 같이 네 가지 키워드들이 존재합니다.

  1. private
  2. public
  3. default (package-private)
  4. protected

기타 제어자에는 다음과 같은 세 가지 키워드들이 존재합니다.

  1. static
  2. final
  3. abstract

이번 글 에서는 기타 제어자보다는 접근 제어자에 집중하도록 하겠습니다.

기타 제어자에 대해 공부하고 싶으신 분들은 다음 링크 참고하시면 좋을 것 같습니다.

기타 제어자 설명

fianl은 정말 불변일까

접근 제어자

private

private으로 선언된 클래스의 멤버는 다른 클래스에서 접근이 불가능합니다.

외부에서 사용되어야 하지 않는 멤버를 private으로 설정해줌으로 협업하는 개발자에게 이런 의도를 잘 전달할 수 있습니다.

또, 모든 접근 제어자 중 접근 할 수 있는 범위가 제일 좁기 때문에 로직이 상대적으로 안전해진다는 장점이 있습니다.

private의 단점으로는 테스트가 어려워진다는 점을 꼽을 수 있습니다.

예시를 하나 들어보겠습니다.

우리는 단일 책임 원칙을 지키기 위해 조회의 기능을 가지지 않고, 명령의 기능만을 가지는 메서드를 작성했습니다.

class Car {
	private int position;
	...
	public void move() {
		position++;
	}
}

move에 대한 테스트는 어떻게 할 수 있을까요?

저는 지금 떠오르는 방법이 세 가지 있습니다. (혹시 더 생각나는 방법이 있다면 의견 남겨주세요!)

  1. move가 예외를 발생시키지 않는지만 테스트한다.
    1. 호출이 정상적으로 완료됐는지는 알 수 있어도 position이 정상적으로 증가했는 지 확인할 수 없겠네요
  2. position에 대한 getter를 만들어 테스트한다
    1. 테스트를 하기 위한 메서드를 만들어냈다는 점이 조금 찝찝하고, 클래스의 캡슐화를 깼다는 점이 많이 찝찝하네요.
  3. extracting또는 reflection을 사용한다.
    1. 위 두 방법보다는 낫지 않나? 라는 생각이 들지만, 테스트코드의 가독성에 문제가 생길 수 있다는 생각이 들고, 멤버 변수 이름이 수정될 때 마다 테스트 코드도 같이 수정해줘야한다는 단점이 생길 것 같습니다.
    2. private으로 만든 의미가 뭐였지? 하고 다시 한 번 생각하게 됩니다.

어떤 방법이든 단점이 떠오릅니다.

테스트가 쉬운 코드를 작성하는 것이 제일 중요하다고 배운 것 같은데, 그럼 private은 사용하면 안될까요?

저는 일단 사용해라 라고 말하고 싶습니다.

일단은 private을 붙여놓고 그때부터 고민하는 방법이 좋다고 생각합니다.

public으로 작성한 코드에 대해서는 컴파일 오류가 잘 발생하지 않기 때문에 예측하지 못한 문제가 발생할 가능성이 높아집니다.

반대로 private으로 작성한 코드는 컴파일 오류가 발생할 확률이 높아집니다. 그 때 고민을 시작하는 것이 좋다고 생각합니다.

과연 이 멤버가 밖에서 사용되면 안될까? 문제가 생길까? 하는 질문을 던지고, 만약 아니라면 점점 범위를 넓혀갈 수 있습니다.

반대로 문제가 생길 것 같다면, 내가 객체나 패키지를 제대로 분리했는지 다시 한 번 생각해 볼 수 있을 것입니다.

테스트 코드를 작성할 때도 동일한 사고를 할 수 있을 것이라고 생각합니다.

public

public 으로 선언된 클래스의 멤버는 어디서든 접근이 가능합니다.

위에서 본 private과 반대의 특성을 지녔기 때문에 장 단점을 반대로 생각해볼 수 있습니다.

저는 지금까지 주로 패키지가 달라지는 컨트롤러에서 사용되는 메서드들을 public으로 오픈하여 사용해왔습니다.

도메인 또는 뷰 에서 내부적으로 밖에서 사용할 준비가 끝났으니 사용해라 라는 메시지를 준다고 이해할 수 있습니다.

나머지 의견에 대해서는 private에서 충분히 설명됐다고 생각하기에 넘어가겠습니다.

default

저에게 가장 생소했던 접근 제어자인 default입니다.

이 제어자로 설정된 멤버들은 같은 패키지 안에서만 사용할 수 있습니다.

default를 통해서는 어떤 의도를 전달할 수 있을까요?

저는 private과 동일한 의도를 전달할 수 있다고 생각합니다.

이 멤버는 밖에서 사용되어서는 안돼! 하고 말이죠. 다만 범위가 패키지일 뿐이라고 생각합니다.

그럼 private대신 default를 사용하기 적절한 상황은 어떤 것일까요?

위에서 본 Car에 대한 정보를 출력하기 위해 View에 Car 객체를 직접 넘겨주었다고 생각해봅시다.

만약 move 메서드가 public이라면 뷰에서 이를 실행할 수 있습니다.

오로지 출력만 할 것이라고 예상했던 곳에서 갑자기 내부의 정보를 수정하고, 이렇게 수정된 잘못된 정보를 출력해버릴 수 있다는 의미입니다.

이럴 때 move 메서드를 default로 바꿔준다면 view에서 Car 객체를 직접 넘겨받아도 이를 실행할 수 없기 때문에 걱정하던 문제를 해결할 수 있습니다.

그런데 여기서 한 가지 문제를 더 떠올릴 수 있습니다.

만약 패키지가 더 세분화되어서 Car는 domain.model 이라는 패키지로 이동해버리고 이를 사용해야하는 RacingGame은 domain.game 이라는 패키지로 이동해버리면 어떡하죠?

이와 같은 문제로 default는 패키지 구조가 자주 변하는 프로그램에서는 적합하지 않을 수 있습니다.

protected

같은 패키지가 아니라면 상속 받은 클래스에서만 해당 멤버를 사용할 수 있게 해주는 접근 제어자입니다.

저는 이 접근제어자가 이런 의도를 전달한다고 생각합니다.

이 멤버는 아직 완성이 아니야. 상속받아서 완성시켜야 해

이 제어자를 주로 어디에서 사용하시나요?

저는 추상 클래스에서 이를 주로 사용합니다.

이번 미션인 chess 에서 저는 Piece라는 추상클래스에 한 가지 protected 멤버 변수를 두었습니다.

public abstract class Piece {

...
    protected final Trace trace;
...
    public abstract boolean canMove(Position source, Position destination);

    public abstract boolean canAttack(Position source, Position destination);
...
}

그리고 이를 구현한 Pawn에서 trace를 사용하였습니다.

 @Override
    public boolean canMove(final Position source, final Position destination) {
        int diffFile = destination.calculateFileDistance(source);
        int diffRank = destination.calculateRankDistance(source);
        if (source.equals(destination) || diffRank != 0) {
            return false;
        }
        if (team == Team.BLACK) {
            return diffFile == -DEFAULT_MOVE_RANGE || (!trace.hasLog() && diffFile == -INITIAL_MOVE_RANGE);
        }
        return diffFile == DEFAULT_MOVE_RANGE || (!trace.hasLog() && diffFile == INITIAL_MOVE_RANGE);
    }

만약 trace(이동기록) 가 존재하지 않는다면 두 칸을 움직일 수 있게 해준 것입니다.

trace를 사용하는 곳이 완성되어있지 않으니 하위 클래스에서 이를 완성할 수 있는 여지를 준 것이죠.

정리

제어자는 내가 작성한 코드의 의도를 명확하게 표현할 수 있게 해주는 역할을 하며, 이를 적절하게 사용하면 로직을 보호해줄 수 있습니다.

접근 제어자의 경우에는 해당 멤버에 접근하는 문의 역할을 합니다.

문을 처음부터 넓게 설정 해놓는다면, 통행하기엔 편하겠지만 내가 생각하지 못하는 통행이 일어날 수 있습니다.

이러한 통행이 일어난 후에 문을 좁히는 일보다는 불편을 감수하고 내가 설계한 통행만 일어나도록 설정해주고, 복합적인 고민을 통해 차차 문을 넓혀가는 편이 좋다고 생각합니다.

이상으로 제어자에 대한 글을 마무리 하겠습니다.

제 생각은 절대 정답이 아닙니다.

다른 의견은 언제나 환영입니다.

profile
overflow

0개의 댓글