Loading the catalog…
Loading the catalog…
캡슐화(Encapsulation)란? 데이터와 그 데이터를 다루는 동작을 하나로 묶고, 내부 사정은 감춘 채 바깥에는 꼭 필요한 기능만 열어두는 것. 여기에는 두 가지 의미가 겹쳐 있다. 하나는 관련된 데이터(필드)와 로직(메서드)을 한 클래스 안에 모으는 '묶기'고, 다른 하나는 그 내부를 바깥에서 함부로 건드리지 못하게 막는 '숨기기'이다. 숨기기 쪽만 따로 떼어서 정보 은닉(information hiding)이라고 부르기도 한다. 자판기를 떠올려보면 쉽다. 우리는 돈을 넣고 버튼을 누르는 것만 할 수 있지, 안에 손을 넣어서 음료를 꺼내거나 동전 개수를 직접 바꿀 수는 없다. 내부에서 재고를 어떻게 관리하고 거스름돈을 어떻게 계산하는지 몰라도 쓰는 데 전혀 지장이 없고, 누가 내부를 망가뜨릴 수도 없다. 이게 캡슐화가 잘 된 상태이다. 캡슐화가 없으면 생기는 일 은행 계좌를 클래스로 예를 들어보자. public class BankAccount{ public int balance; // 누구나 직접 읽고 쓸 수 있음 } //프로젝트 어딘가에서.. account.balance = -1000000; // 잔액이 마이너스가 돼도 아무도 못 막음 account.balance += account; // 입금 로직이 코드 곳곳에 흩어짐 문제는 크게 2가지이다. 첫째, "잔액은 음수가 될 수 없다"같은 규칙을 강제할 방법이 없다. 둘째, 나중에 "입금할 때마다 로그를 남겨야 한다"는 요구사항이 생기면 balance를 건드리는 코드를 프로젝트 전체에서 찾아내서 전부 고쳐야 한다. 하나라도 빠뜨리면 그게 바로 버그가 된다. 캡슐화를 제대로 적용하려면 public class BankAccount{ private int balance; // 클래스 밖에서는 직접 접근 불가 public void deposit(int amount) { if (amount <= 0) { throw new IllegalArgumentException("입금액은 0보다 커야 합니다."); } balance += amount; public void withdraw(int amount){ if (amount <= 0 || amount > balance) { balance -= amount; } public int getBalance() { return balance; } } 이제 balance는 private이라 밖에서 손댈 수 없고, 잔액을 바꾸는 방법은 deposit()과 withdraw()를 거치는 것 뿐이다. 그 두 메서드가 규칙을 검사하여, 누가 이 객체를 어떻게 쓰든 잔액이 음수가 되는 일은 생길 수 없다. 이렇게 객체가 언제나 지켜야 하는 조건을 불변식(invariant)이라고 하는데, 캡슐화의 가장 큰 목적이 바로 객체가 자기 불변식을 스스로 지키게 만드는 것이다. 부수적인 이득도 크다. 로그를 남겨야 하면 deposit() 한 곳만 고치면 되고, 잔액이 이상하게 나오면 이 클래스의 메서드 몇 개만 살펴보면 된다. 더 나아가서 나중에 잔액을 int필드 대신 거래 내역 목록으로 저장하고 합계를 계산하는 방식으로 내부를 통째로 갈아엎어도, 공개된 메서드의 모양만 그대로면 이 클래스를 쓰는 코드는 한 줄도 바꿀 필요가 없다. 변경의 영향이 클래스 경계 안에서 멈춘다. 이걸 어려운 말로 "결합도를 낮춘다"라고 한다. getter/setter만 붙이면 캡슐화일까? 필드를 private으로 바꾸고 getter/setter 붙이면 캡슐화 끝 <- 이라고 초반에 생각하기 쉬운데, 이건 반만 맞다. public class BankAccount { private int balance; public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } } 위의 코드를 보면, 필드는 분명 private인데 account.setBalance(-1000000) 이 여전히 가능하다. 한 단계 거쳐갈 뿐, public필드랑 똑같이 아무것도 보호하지 못하고 있따. 캡슐화의 핵심은 필드를 숨기는 문법이 아니라, 데이터를 꺼내주는 대신 객체에게 일을 시키는 구조를 만드는 데 있다. 이걸 "Tell, Don't Ask(묻지 말고 시켜라)" 원칙이라고 부른다. // Ask: 데이터를 꺼내서 밖에서 판단하고 조작 if (account.getBalance() >= price) { account.setBalance(account.getBalance() - price); } // Tell: 객체에게 할 일을 시킴 account.withdraw(price); Ask방식은 "잔액이 충분한가"라는 규칙이 계좌 클래스 바깥에 있어서, 결제하는 곳마다 같은 검사를 반복해야 하고 결국 누군가는 빼을 수 있다. 반면, Tell방식은 규칙이 객체 안에 딱 한 번만 존재한다. 오해하지 말아야 할 건 getter 자체가 나쁜 건 아니다. 잔액을 화면에 보여주는 것처럼 값을 읽기만 하는 용도로만 괜찮다. 문제는 꺼낸 값으로 밖에서 판단하고 상태를 바꾸는 코드이다. 같은 이유로 메서드 이름도 데이터 조작이 아니라 의도가 드러나게 짓는 게 좋다. order.setStatus(OrderStatus.CANCELLED) 보다 order.cancel() 이 훨씬 나은데, cancel() 안에서는 "이미 배송된 주문은 취소할 수 없다"같은 규칙을 자연스럽게 검사할 수 있따. setStatus()는 어떤 값이든 받아주는 통로일 뿐이다. 놓치기 쉬운 구멍: 내부 컬렉션 노출 필드를 private으로 잘 막아놨는데도 캡슐화가 새는 경우가 있다. public class Order { private final List<OrderItem> items = new ArrayList<>(); public List<OrderItem> getItems() { return items; // 내부 리스트를 그대로 넘겨줌 } } order.getItems().clear(); // 밖에서 주문 항목을 통째로 지울 수 있다 getter가 내부 리스트의 참조를 그대로 넘기면, 받은 쪽에서 add()나 clear()로 계속 속을 마음대로 바꿀 수 있다. 여기서 final은 "다른 리스트로 바꿔치기할 수 없다"는 뜻이지 "리스트 내용을 못 바꾼다"는 뜻이 아니라서 도움이 안된다. 이럴 땐 읽기 전용으로 감싸서 돌려주고, 추가는 검증이 들어간 전용 메서드로만 하게 만든다. public List<OrderItem> getItems() { return Collections.unmodifiableList(items); // 수정 시도 시 예외 발생 } public void addItem(OrderItem item) { // 수량 확인, 최대 개수 제한 같은 규칙을 여기서 검사 items.add(item); } List뿐 아니라 Map이나 배열처럼 내용을 바꿀 수 있는 객체를 반환할 때는 항상 같은 문제를 의식해야한다. 접근 제어자, 그리고 다른 언어 Java에서 캡슐화를 구현하는 도구가 접근 제어자이다. private은 같은 클래스 안에서만, 아무것도 안 붙인 default(package-private)는 같은 패키지 안에서만, protected는 같은 패키지와 상속받은 자식 클래스에서, public은 어디서나 접근할 수 있다. 실무 원칙은 단순하다. 일단 가장 좁은 private으로 시작하고, 정말 필요할 때만 넓힌다. 한번 public으로 열어둔 건 누가 어디서 쓰고 있을지 몰라서 나중에 닫기가 정말 어렵다. 다른 언어도 방식만 다를 뿐 생각은 같다. Python은 언어 차원에서 접근을 막아주지 않아서 _balance 처럼 밑줄을 붙여 "내부용이니 건드리지마"라는 약속으로 표현하고, JavaScript는 #balance 처럼 #을 붙이면 진짜 private 필드가 된다. 무분별한 게터/세터 모든 private 속성에 getter와 setter를 만들어 public으로 열어 놓는다면 외부에서 언제든 값을 꺼내고 변경이 자유롭다. 이런 부분을 무분별한 게터/세터 라고 표현한다. 무분별한 게터/세터가 쓰이게 되면 캡슐화가 깨지게 된다. 캡슐화의 핵심은 데이터 은닉이지만 좀 더 풀어서 설명하면 -> 객체의 속성은 객체가 정한 규칙대로 바뀌게 허용한다 이다. 게터(Getter) 캡슐화가 무너져 있는 예시 List<Integer> resultsList = calculator.getResults(); resultList.add(1); // Calculator 클래스 내부의 속성을 외부에서 조작 위의 코드는 캡슐화가 지켜진 코드가 아니다. 캡슐화를 지키면서 조회하는 방법은 아래의 형식으로 수정이 불가능한 복제본을 만들어서 반환해주는 방법이 있다. 이런 방법을 방어적 복사 라고 한다. public List<Integer> getResults() { return List.copyOf(resultList); } 반환 방식 | 받은 쪽에서 add()하면 | 내부 리스트 | 나중에 내부가 바뀌면 받은 리스트는| |--|--|--|--| return resultList; | 성공 | 같이 바뀜(캡슐화 깨짐) | 같이 바뀜| return new ArrayList<>(resultList); | 성공(사본만 바뀜) | 그대로 | 안바뀜| return Collections.unmodifiableList(resultList); | 예외 발생 | 그대로 | 같이 바뀜| return List.copyOf(resultList); |예외 발생|그대로|안 바뀜| List.copyOf(resultList) 는 이 문제를 두 겹으로 막아준다. 첫째, 이름은 그대로 새 리스트를 만들어서 원본의 요소를 옮겨 담는다. 그래서 받은 쪽이 쥐는 건 원본과 분리된 사본이다. 둘째, 그 사본은 수정이 불가능한 리스트이다. 실수로 고치려 해도 원본은 절대 안 바뀌고, 고치려는 시도는 바로 예외로 드러난다. 내부 상태를 지키려고 미리 복사해서 내보낸다고 해서 방어적 복사라고 부른다. Collections.unmodifiableList 도 외부 수정을 막는다는 점은 같다. 다만 이건 사본이 아니라 원본을 읽기 전용으로 들여다보는 창(view)이다. 그래서 Calcualtor 안에 나중에 결과가 추가 되면, 이미 받아 간 리스트에서도 그게 보인다. 대신 복사를 안하니 비용은 들지 않는다. new ArrayList<>(resultList) 는 반대로 복사는 하지만 수정은 가능. 받은 쪽이 add()를 하면 사본만 조용히 바뀐다. 캡슐화는 지켜지지만, 호출한 쪽은 결과를 바꿨다고 착각할 수 있다. 계산 결과처럼 "그 시점의 목록을 보여주는" 용도라면 List.copyOf 가 제일 무난하다. Collections.unmodifiableList VS List.copyOf Collections.unmodifiableList 는 원본을 감싼 읽기 전용 창(view)이다. 이 창을 통해서는 못 고치지만, 창 너머의 원본이 바뀌면 창에 보이는 내용도 같이 바뀐다. 즉 "이 참조로는 못 고친다"는 뜻이지, "내용이 절대 안바뀐다"는 뜻은 아니다. List<Integer> results = calculator.getResults(); // 뷰(unmodifiableList)를 받음 System.out.println(results); // [3, 7] // ...그 뒤 새 계산을 해서 Calculator 내부 리스트에 10이 추가되면 System.out.println(results); // [3, 7, 10] ← 받아 둔 리스트의 내용이 바뀌어 있음 List.copyOf 로 받았다면 두 번째 출력도 [3,7]이다. 이 차이가 실제 문제로 번지는 대표적인 경우가 순회이다. for (Integer result : calculator.getResults()) { // 뷰를 순회하는 도중에 if (result < 0) { calculator.removeFirstResult(); // 원본이 바뀌면 ConcurrentModificationException } } 뷰는 원본을 그대로 들여다보고 있다. 그래서 순회 중에 원본이 바뀌면 Java가 이를 감지해서 ConcurrentModificationException을 던질 수 있다. copyOf로 받은 사본을 들고 있었다면, 원본이 바뀌어도 사본은 그대로라 안전하다. 받아 둔 목록이 나도 모르게 바뀌는 것 자체도 디버길할 때 꽤 헷갈리는 부분 중 하나가 될 수 있다. 대신 뷰에는 확실한 장점이 있다. unmodifiableList는 감싸기만 하니까 리스트가 아무리 커도 비용이 거의 없다. 반면 copyOf는 호출할 때마다 요소 수만큼 복사한다. 그래서 리스트가 아주 크고 getter가 자주 불리는데 받은 쪽이 바로 읽고 버린다면 뷰가 더 효율적이다. 항상 최신 상태를 보여주는 창이 필요할 때도 뷰가 오히려 맞는 선택이다. 세터(Setter) 세터 또한 기본적으로 만들지 않는 것을 권장 setResults()처럼 속성을 통빼로 바꾸는 메서드 대신, 외부에서 실제로 필요한 동작만 메서드로 열어줘야 한다.
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
[TIL]2026.9.30.. 캡슐화(Encapsulation)란? 데이터와 그 데이터를 다루는 동작을 하나로 묶고, 내부 사정은 감춘 채 바깥에는 꼭 필요한 기능만 열어두는 것. 여기에는 두 가지 의미가 겹쳐 있다. 하나는 관련된 데이터(필드)와 로직(메서드)을 한 클래스 안에 모으는 '묶기'고, 다른 하나는 그 내부를 바깥에서 함부로 건드리지 못하게 막는 '숨기기'이다. 숨기기 쪽만 따로 떼어서 정보 은닉(information hiding)이라고 부르기도 한다. 자판기를 떠올려보면 쉽다. 우리는 돈을 넣고 버튼을 누르는 것만 할 수 있지, 안에 손을 넣어서 음료를 꺼내거나 동전 개수를 직접 바꿀 수는 없다. 내부에서 재고를 어떻게 관리하고 거스름돈을 어떻게 계산하는지 몰라도 쓰는 데 전혀 지장이 없고,…
Open source