Information Security Study
240129 자바 객체지향 문법/코드(toString(), equals(), Optional/if문 자제, Getter, Setter) 본문
240129 자바 객체지향 문법/코드(toString(), equals(), Optional/if문 자제, Getter, Setter)
gayeon_ 2024. 1. 29. 15:55Object 클래스
toString()
- 인스턴스를 콘솔에 찍었을때 찍힐 내용을 적는 메서드
- 디버깅, 로깅 등에서 인스턴스에 무슨 값이 들어있었나 체크하기 위해서 사용한다.
- 로그를 남기는 이유는 문제 해결, 알아차리기, 법적 이슈로 남기는 경우가 있다.
toString() 예제
package object.tostring;
public class SomeObject {
private int intField;
private String stringField;
public SomeObject(int intField, String stringField) {
this.intField = intField;
this.stringField = stringField;
}
// 우클릭 source -> generate toString()
@Override
public String toString() {
return "SomeObject [intField=" + intField + ", stringField=" + stringField + "]";
}
}
package object.tostring;
public class ToStringExampleMain {
public static void main(String[] args) {
SomeObject someObject1 = new SomeObject(1, "pppp");
SomeObject someObject2 = new SomeObject(2, "qqqq");
// toString() 에서 리턴하는 문자열이 콘솔에 찍힌다.
// 재정의하지 않았다면 레퍼런스 주소만 찍힌다.
System.out.println(someObject1);
System.out.println(someObject2);
}
}
toString()를 재정의하지 않았다면 각각 다른 레퍼런스 주소가 출력되고
toString()를 재정의했다면 toString()에서 리턴하는 문자열이 출력된다.
.equals()
- 동일성 vs 동등성
- 동일성을 두 대상이 “똑같은 대상” 이어야 성립하는데 == 를 이용해 비교해 검사한다.
- 동등성은 대상은 다르지만 어떤 다른 기준에 의해 같음을 확인했을 때 성립하는데 이 때 equals()를 활용한다.
인텔리제이에서 동등성 메서드 재정의 자동으로 하는 방법
(우클릭 → generate → equals and hashcode → java7+)
.equals() 예제
package object.equals;
public class EqualsExampleMain {
public static void main(String[] args) {
SomeObject someObject1 = new SomeObject(1, "asdf");
SomeObject someObject2 = new SomeObject(1, "asdf");
SomeObject anotherObject = new SomeObject(99, "qwer");
// 동일성 비교(레퍼런스 주소 비교)
System.out.println(someObject1 == someObject2);
// 동등성 비교(equals를 오버라이딩한 기준에 따라 비교)
System.out.println(someObject1.equals(someObject2));
// 동등성 비교
System.out.println(someObject1.equals(anotherObject));
}
}
package object.equals;
import java.util.Objects;
public class SomeObject {
private int intField;
private String stringField;
public SomeObject(int intField, String stringField) {
this.intField = intField;
this.stringField = stringField;
}
public int getIntField() {
return intField;
}
public String getStringField() {
return stringField;
}
@Override
public boolean equals(Object o) {
// 참조주소가 같으면 int와 string이 같은지 비교할 필요 없이 바로 참으로 인식
if(this == o) return true;
// 유효하지 않은 Object o를 집어넣었다면 이 역시 비교대상이 될 수 없으므로 바로 거짓으로 인식
if(o == null || getClass() != o.getClass()) return false;
// 파라미터로 받은 Object는 다형성 원리에 의해서 부모클래스 위치에 SomeObject라는 자식 객체를
// 받았을 것이므로 먼저 다시 SomeObject로 자료형을 캐스팅해 준 다음
SomeObject that = (SomeObject) o;
// 캐스팅되어 SomeObject로 돌아온 객체와 현재 비교하려는 객체의 intField와 stringField를 비교해
// 모두 일치하는 경우에 true를 리턴하도록 구성
return intField == that.intField && Objects.equals(stringField, that.stringField);
}
}
- 참조주소가 같으면 int와 string이 같은지 비교할 필요 없이 바로 참으로 인식
- 유효하지 않은 Object o를 집어넣었다면 이 역시 비교대상이 될 수 없으므로 바로 거짓으로 인식
- 파라미터로 받은 Object는 다형성 원리에 의해서 부모클래스 위치에 SomeObject라는 자식 객체를
- 받았을 것이므로 먼저 다시 SomeObject로 자료형을 캐스팅해 준 다음
- 캐스팅되어 SomeObject로 돌아온 객체와 현재 비교하려는 객체의 intField와 stringField를 비교해 모두 일치하는 경우에 true를 리턴하도록 구성
오버라이딩 전
false
false
false
오버라이딩 후
false
true
false
Optional
자바에서 null 처리를 쉽게 처리하기 위해 부가적으로 나온 자료형
없어도 자바 코드만으로도 같은 기능을 구현할 수 있지만, 좀 더 가독성 높게 처리할 수 있다.
public Optional<자료형> 메서드명() {
return Optional.ofNullable(자료);
}
위와 같이 Optional로 다른 자료형을 감싸주면 Optional 자료형을 쓸 수 있다.
이렇게 되면 좀 더 세련된(?) 코드를 작성할 수 있다.
Optional 자료형에 대해서는 아래와 같이 예외처리 코드를 작성할 수 있다.
.orElseThrow(() → new 예외클래스())
Optional 객체를 만드는 정적 팩토리 패턴 메서드 명령어
Optional.of(자료); // 생성 시점에서 null이면 예외 발생
Optional.ofNullable(자료); // 생성 시점에서는 null여부 상관없지만 처리가 쉬움
Optional.empty(); // 빈 옵셔널 만들고 싶은 경우(거의 쓸 일 없음)
JPA같이 Optional로 된 자료를 기본으로 리턴하는 경우 orElseThrow 같은 메서드로 예외를 처리하면 된다.
만약 그런데 일반 객체만 리턴하는 경우라면 Optional.ofNullable(자료)를 이용해 생성한 다음 orElseThrow를 적용하면 된다.
Optional을 사용하지 않은 예제
package optional.without_optional;
import java.util.HashMap;
import java.util.Map;
public class MapRepository {
private Map<String, String> map = new HashMap<>();
public MapRepository() {
map.put("EXIST_KEY", "value");
}
public String getValue(String key) {
return map.get(key);
}
}
package optional.without_optional;
public class WithoutOptionalExampleMain {
public static void main(String[] args) {
MapRepository mapRepository = new MapRepository();
// 존재하지 않는 키로 조회(결과가 null)
String string = mapRepository.getValue("NOT_EXIST_KEY");
System.out.println("string= " + string);
// 대문자로 바꿔서 출력해야 하지만, NullPointerException 발생
// System.out.println(string.toUpperCase());
if(string != null) { // 조건문으로 널 검사 후에
System.out.println(string.toUpperCase());
}
}
}
if문으로 검사하는 건 좋지 않은 예시이다.
Optional을 사용한 예제
package optional.with_optional;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class MapRepository {
private Map<String, String> map = new HashMap<>();
public MapRepository() {
map.put("EXIST_KEY", "value");
}
public Optional<String> getOptionalValue(String key) {
return Optional.ofNullable(map.get(key));
}
public String getValue(String key) {
return map.get(key);
}
}
package optional.with_optional;
import javax.management.RuntimeErrorException;
public class WithOptionalExampleMain {
public static void main(String[] args) {
MapRepository mapRepository = new MapRepository();
String string = mapRepository.getOptionalValue("NOT_EXIST_KEY")
.orElseThrow(() -> {throw new RuntimeException("없는 키 값입니다.");});
// 해당 키로 조회한 결과가 null 인데 옵셔녈 반환을 받은 경우 예외 발생
// 내부에 람다식으로 () -> { throw new 발생시킬예외("메세지"); }
// 위와 같이 작성하면 해당 예외가 정상값이 아닌 null이 들어왔을 때 발생한다.
System.out.println(string.toUpperCase());
}
}
객체지향적 코드 작성하기
if문을 최대한 억제하기
많은 if문은 개발자의 인지능력을 흐뜨려놓는다.
calculate 관련 코드에서 잘 동작하는것처럼 보였으나
실제로는 null체크와 0 여부 확인을 해야하는데 이런 경우 몇 가지 방법으로 if문을 줄일 수 있다.
- early return : 빠르게 리턴한다.
- 미리 유효성 검증 후 연산하기
- enum 안으로 연산을 집어넣기
그러나 근본적으로 코드실행중에 유효성을 검사하는것은 if문을 계속 양산한다.
따라서 생성 시점에 아예 유효성을 검사해버리는 것이 더 좋다.
if문을 많이 사용했을 때의 예제
package lessif.many_if;
public enum CalculateType {
ADD, MINUS, MULTIPLY, DIVIDE
}
package lessif.many_if;
public class CalculateCommand {
private CalculateType calculateType;
private int num1;
private int num2;
public CalculateCommand(CalculateType calculateType, int num1, int num2) {
this.calculateType = calculateType;
this.num1 = num1;
this.num2 = num2;
}
public CalculateType getCalculateType() {
return calculateType;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
package lessif.many_if;
public class Client {
public int someMethod(CalculateCommand calculateCommand) {
CalculateType calculateType = calculateCommand.getCalculateType();
int num1 = calculateCommand.getNum1();
int num2 = calculateCommand.getNum2();
int result = 0;
// 1. CalculateType이 null이라면 연산이 되지 않는다. -> null 검사를 매 로직마다
// 2. DIVIDE 시에 num2가 0이면? -> 0인지 검사해서 0이면 예외 발생, 아니면 처리
// 위 2개 이슈를 해결할 수 있는 방식으로 if문 고치기
if(calculateType != null && calculateType.equals(CalculateType.ADD)) {
result = num1 + num2;
} else if(calculateType != null && calculateType.equals(CalculateType.MINUS)) {
result = num1 - num2;
}
else if(calculateType != null && calculateType.equals(CalculateType.MULTIPLY)) {
result = num1 * num2;
}
else if(calculateType != null && calculateType.equals(CalculateType.DIVIDE)) {
if(num2 != 0) {
result = num1 / num2;
} else {
throw new RuntimeException("0으로 나눌 수 없습니다.");
}
}
return result;
}
}
위 코드의 두 가지 문제점
- CalculateType이 null이라면 연산이 되지 않지만 null 검사를 매 로직마다 수행해야 한다.
- DIVIDE 시에 num2가 0이라면 0인지 검사해서 0이면 예외 발생, 아니면 처리해야 한다.
Client는 계산을 직접 수행하는 로직을 가지고 있을 필요가 없다. -> 가지고 있다면 객체지향적인 코드가 아니다.
예를 들어 계산기에 값 입력만 받는 것 뿐만 아니라 계산 공식까지 알려주는 것과 비슷하다. (객체지향X)
early_return 예제
CalculateCommand와 CalculateType은 동일하고 Client 코드만 다르다.
package lessif.early_return;
public class Client {
public int someMethod(CalculateCommand calculateCommand) {
CalculateType calculateType = calculateCommand.getCalculateType();
int num1 = calculateCommand.getNum1();
int num2 = calculateCommand.getNum2();
int result = 0;
// early return은 핵심 로직 수행 전에 아예 리턴구문과 함께 앞쪽에 검사로직을 배치하는 것이다.
// return이 실행되는 순간 메서드가 종료되기 때문에 핵심 로직을 불완전한 상태로 수행하지 않는다.
// if문을 되도록 쓰지 말자는 것이지 if문을 불가피한 상황에서도 쓰지 말자는 의미는 아니다.
if(calculateType == null) { // null인 경우는 바로 리턴해서 추가 로직 실행 방지
return result;
}
// num2가 0인지 아닌지 여부만 확인하면 더하기 빼기 곱하기 등 0이어도 상관없는 연산까지 막으므로
// 조건식이 DIVIDE이면서 동시에 0인 경우만 exception 발생시키기
if(calculateType.equals(calculateType.DIVIDE) && num2 == 0) {
throw new RuntimeException("0으로 나눌 수 없습니다.");
}
if(calculateType.equals(calculateType.ADD)) {
result = num1 + num2;
} else if(calculateType.equals(calculateType.MINUS)) {
result = num1 - num2;
} else if(calculateType.equals(calculateType.MULTIPLY)) {
result = num1 * num2;
} else if(calculateType.equals(calculateType.DIVIDE)) {
result = num1 / num2;
}
return result;
}
}
early return은 핵심 로직 수행 전에 아예 리턴구문과 함께 앞쪽에 검사로직을 배치하는 것이다.
return이 실행되는 순간 메서드가 종료되기 때문에 핵심 로직을 불완전한 상태로 수행하지 않는다.
if문을 되도록 쓰지 말자는 것이지 if문을 불가피한 상황에서도 쓰지 말자는 의미는 아니다.
num2가 0인지 아닌지 여부만 확인하면 더하기 빼기 곱하기 등 0이어도 상관없는 연산까지 막으므로
조건식이 DIVIDE이면서 동시에 0인 경우만 exception 발생시키기
생성자에서 체크하는 예제
package lessif.check_in_constructor;
public class CalculateCommand {
private CalculateType calculateType;
private int num1;
private int num2;
public CalculateCommand(CalculateType calculateType, int num1, int num2) {
// 생성자 내부에서 1. claculateType이 null인지 여부를
// 2. DIVIDE 연산인 경우 num2가 0인지 여부를 확인한다.
if(calculateType == null) {
throw new RuntimeException("CalculateType은 필수 값 입니다.");
}
if(calculateType == CalculateType.DIVIDE && num2 == 0) {
throw new RuntimeException("0으로 나눌 수 없습니다.");
}
this.calculateType = calculateType;
this.num1 = num1;
this.num2 = num2;
}
public CalculateType getCalculateType() {
return calculateType;
}
public int getNum1() {
return num1;
}
public int getNum2() {
return num2;
}
}
package lessif.check_in_constructor;
public class Client {
public int someMethod(CalculateCommand calculateCommand) {
CalculateType calculateType = calculateCommand.getCalculateType();
int num1 = calculateCommand.getNum1();
int num2 = calculateCommand.getNum2();
int result = 0;
if(calculateType.equals(calculateType.ADD)) {
result = num1 + num2;
} else if(calculateType.equals(calculateType.MINUS)) {
result = num1 - num2;
} else if(calculateType.equals(calculateType.MULTIPLY)) {
result = num1 * num2;
} else if(calculateType.equals(calculateType.DIVIDE)) {
result = num1 / num2;
}
return result;
}
}
package lessif.check_in_constructor;
public class CheckInConstructorMain {
public static void main(String[] args) {
// CalculateCommand calculateCommand = new CalculateCommand(null, 2, 3);
// CalculateCommand calculateCommand = new CalculateCommand(CalculateType.DIVIDE, 3, 0);
CalculateCommand calculateCommand = new CalculateCommand(CalculateType.DIVIDE, 512, 17);
Client client = new Client();
int result = client.someMethod(calculateCommand);
System.out.println("연산의 결과는: " + result);
}
}
생성자 내부에서
1. claculateType이 null인지 여부를
2. DIVIDE 연산인 경우 num2가 0인지 여부를 확인한다.
실행 조건을 생성자 내에서 판단하기 때문에
앞선 예제들보다는 객체지향적이다.
여전히 client에서 직접 계산하는 로직은 잘못된 부분이다.
Getter, Setter
getter가 작성되었을때의 문제
클라이언트 코드에서 커맨드 코드 내부를 쉽게 추론하도록 만들어주는 문제가 있다.
따라서 getter대신 특정 비즈니스 로직을 getter대신 바로 집어넣는 방법이 더 좋다.
캡슐화는 이런 방향으로 어떤 작업을 할 수 있지는 알려주지만, 그 작업이 어떤 방식으로 진행되는지는 숨기는 방향으로 간다.
이를 결합도와 응집도라는 단어로 설명할 수 있다.
결합도
: 두 클래스간에 영향을 주는 것이 많아서(하나의 클래스를 다른 클래스에서 과도하게 참조해서) 하나의 변경이 다른 클래스에도 영향을 크게 주는 경우
: 10줄 호출할 것을 5줄 호출로 줄였다면 결합도가 높아진 것
응집도
: 하나의 도메인에서 활용하는 로직을 최대한 하나의 클래스 내부에 모아두는 경우
객체지향에서는 결합도는 낮추고 응집도는 높여야 좋다고 한다.
'네트워크 캠퍼스 > JAVA' 카테고리의 다른 글
240130 자바(getter/setter의 문제점, stream API, Optional) (0) | 2024.01.30 |
---|---|
240125 자바(객체지향 이론 및 문법, 추상클래스, 인터페이스) (1) | 2024.01.25 |
240124 자바(프로세스와 쓰레드) (0) | 2024.01.25 |
240123 자바 API(제네릭, collection, List, ArrayList, LinkedList, Set, Iterator, HashSet, Map, HashMap) (1) | 2024.01.23 |
240122 자바 API(Wrapper, java.util, Arrays, Date, SimpleDateFormat, Calendar, Random) (0) | 2024.01.22 |