Now Loading ...
-
☕️[Java] Math, Random 클래스
Math, Random 클래스
Math 클래스
Math는 수 많은 수학 문제를 해결해주는 클래스입니다.
너무 많은 기능을 제공하기 때문에 대략 이런 것이 있구나 하는 정도면 충분합니다.
실제 필요할 때 검색하거나 API 문서를 찾아봅시다.
1. 기본 연산 메서드
abs(x) : 절대값
max(a, b) : 최대값
min(a, b) : 최소값
2. 지수 및 로그 연산 메서드
exp(x) : e^x 계산
log(x) : 자연 로그
log10(x) : 로그 10
pow(a, b) : a의 b 제곱
3. 반올림 및 정밀도 메서드
ceil(x) : 올림
floor(x) : 내림
rint(x) : 가장 가까운 정수로 반올림
round(x) : 반올림
4. 삼각 함수 메서드
sin(x) : 사인
cos(x) : 코사인
tan(x) : 탄젠트
5. 기타 유용한 메서드
sqrt(x) : 제곱근
cbrt(x) : 세제곱근
random() : 0.0과 1.0 사이의 무작위 값 생성
Math에서 자주 사용하는 기능들을 예제로 만들어서 실행해봅시다.
package lang.math;
public class MathMain {
public static void main(String[] args) {
System.out.println("max(10, 20): " + Math.max(10, 20)); // 최대값
System.out.println("min(10, 20): " + Math.min(10, 20)); // 최소값
System.out.println("abs(-10): " + Math.abs(-10)); // 절대값
// 반올림 및 정밀도 메서드
System.out.println("ceil(2.1): " + Math.ceil(2.1)); // 올림
System.out.println("floor(2.1): " + Math.floor(2.1)); // 내림
System.out.println("round(2.5): " + Math.round(2.5)); // 반올림
// 기타 유용한 메서드
System.out.println("sqrt(4): " + Math.sqrt(4)); // 제곱근
System.out.println("random(): " + Math.random()); // 0.0 ~ 1.0 사이의 double 값을 반환
}
}
실행 결과
max(10, 20): 20
min(10, 20): 10
abs(-10): 10
ceil(2.1): 3.0
floor(2.1): 2.0
round(2.5): 3
sqrt(4): 2.0
random(): 0.45992290098234856
참고 : 아주 정밀한 숫자와 반올림 계산이 필요하다면 BigDecimal을 검색해봅시다.
Random 클래스
랜덤의 경우 Math.random()을 사용해도 되지만 Random 클래스를 사용하면 더욱 다양한 랜덤값을 구할 수 있습니다.
참고로 Math.random()도 내부에서는 Random 클래스를 사용합니다.
참고로 Random 클래스는 java.util 패키지 소속입니다.
package lang.math;
import java.util.Random;
public class RandomMain {
public static void main(String[] args) {
Random random = new Random();
int randomInt = random.nextInt();
System.out.println("randomIntL " + randomInt);
double randomDouble = random.nextDouble(); // 0.0d ~ 1.0d 사이값이 출력됨
System.out.println("randomDouble: " + randomDouble);
boolean randomBoolean = random.nextBoolean();
System.out.println("randomBoolean: " + randomBoolean);
// 범위 조회
int randomRange1 = random.nextInt(10); // 0 ~ 9까지 출력
System.out.println("0 ~ 9: " + randomRange1);
int randomRange2 = random.nextInt(10) + 1; // 1 ~ 10까지 출력
System.out.println("1 ~ 10: " + randomRange2);
}
}
실행 결과
실행 결과는 항상 다르다.
randomIntL -1662566800
randomDouble: 0.14362030528813963
randomBoolean: false
0 ~ 9: 4
1 ~ 10: 10
random.nextInt(): 랜덤 int 값을 반환합니다.
nextDouble() : 0.0d ~ 1.0d 사이의 랜덤 double 값을 반환합니다.
nextBoolean() : 랜덤 boolean 값을 반환합니다.
nextInt(int bound): 0 ~ bound 미만의 숫자를 랜덤으로 반환합니다. 예를 들어서 3을 입력하면 0, 1, 2를 반환합니다.
1부터 특정 숫자의 int 범위를 구하는 경우 nextInt(int bound)의 결과에 +1을 하면 됩니다.
씨드 - Seed
랜덤은 내부에서 씨드(Seed) 값을 사용해서 랜덤 값을 구합니다.
그런데 이 씨드 값이 같으면 항상 같은 결과가 출력됩니다.
// Random random = new Random();
Random random = new Random(1); // seed가 같으면 Random의 결과가 같다.
실행 결과
Seed가 같으면 실행 결과는 반복 실행해도 같습니다.
randomIntL -1155869325
randomDouble: 0.10047321632624884
randomBoolean: false
0 ~ 9: 4
1 ~ 10: 5
new Random(): 생성자를 비워두면 내부에서 System.nanoTime()에 여러가지 복잡한 알고리즘을 섞어서 씨드값을 생성합니다.
따라서 반복 실행해도 결과가 항상 달라집니다.
new Random(int seed): 생성자에 씨드 값을 직접 전달할 수 있습니다. 씨드 값이 같으면 여러번 반복 실행해도 실행 결과가 같습니다.
이렇게 씨드 값을 직접 사용하면 결과 값이 항상 같기 때문에 결과가 달라지는 랜덤 값을 구할 수 없습니다.
하지만 결과가 고정되기 때문에 테스트 코드 같은 곳에서 같은 결과를 검증할 수 있습니다.
참고로 마인크래프트 같은 게임은 게임을 시작할 때 지형을 랜덤으로 생성하는데, 같은 씨드값을 설정하면 같은 지형을 생성할 수 있습니다.
-
☕️[Java] System 클래스
System 클래스
System 클래스는 시스템과 관련된 기본 기능들을 제공합니다.
package lang.system;
import java.util.Arrays;
public class SystemMain {
public static void main(String[] args) {
// 현재 시간(밀리초)를 가져옵니다.
long currentTimeMillis = System.currentTimeMillis();
System.out.println("currentTimeMillis = " + currentTimeMillis);
// 현재 시간(나노초)를 가져옵니다.
long currentTimeNano = System.nanoTime();
System.out.println("currentTimeNano = " + currentTimeNano);
// 환경 변수를 읽습니다.
System.out.println("getenv = " + System.getenv());
// 시스템 속성을 읽습니다.
System.out.println("properties = " + System.getProperties());
System.out.println("Java version = " + System.getProperty("java.version"));
// 배열을 고속으로 복사합니다.
char[] originalArray = { 'h', 'e', 'l', 'l', 'o' };
char[] copiedArray = new char[5];
System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length);
// 배열 출력
System.out.println("copiedArray = " + copiedArray);
System.out.println("Arrays.toString = " + Arrays.toString(copiedArray));
// 프로그램 종료
System.exit(0);
}
}
실행 결과
currentTimeMillis = 1713485140558
currentTimeNano = 694481339313708
getenv = {HOMEBREW_PREFIX=/opt/homebrew, MANPATH=/Users/kobe/.nvm/versions/node/v20.10.0/share/man:/opt/local/share/man:/opt/homebrew/share/man::, COMMAND_MODE=unix2003, INFOPATH=/opt/homebrew/share/info:...
properties = {java.specification.version=21, sun.jnu.encoding=UTF-8, java.class.path=/Users/kobe/Desktop/practiceJavaMidPart1/java-mid1/out/production/java-mid1, java.vm.vendor=Oracle Corporation, sun.arch.data.model=64, java.vendor....
Java version = 21.0.2
copiedArray = [C@77459877
Arrays.toString = [h, e, l, l, o]
Process finished with exit code 0
표준 입력, 출력, 오류 스트림: System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 스트림을 나타냅니다.
시간 측정: System.currentTimeMillis()와 System.nanoTime()은 현재 시간을 밀리초 또는 나노초 단위로 제공합니다.
환경 변수: System.getProperties()를 사용해 현재 시스템 속성을 얻거나 System.getProperty(Stringkey)로 특정 속성을 얻을 수 있습니다. 시스템 속성은 자바에서 사용하는 설정 값입니다.
시스템 종료: System.exit(int status) 메서드는 프로그램을 종료하고, OS에 프로그램 종료의 상태 코드를 전달합니다.
상태 코드0 : 정상 종료
상태 코드 0이 아님: 오류나 예외적인 종료
배열 고속 복사: System.arraycopy는 시스템 레벨에서 최적화된 메모리 복사 연산을 사용합니다. 직접 반복문을 사용해서 배열을 복사할 때 보다 수 배 이상 빠른 성능을 제공합니다.
-
☕️[Java] Class 클래스
Class 클래스.
자바에서 Class 클래스는 클래스의 정보(메타데이터)를 다루는데 사용됩니다.
Class 클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있습니다.
Class 클래스의 주요 기능은 다음과 같습니다.
타입 정보 얻기: 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있습니다.
리플렉션: 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 등의 작업을 할 수 있습니다.
동적 로딩과 생성: Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성할 수 있습니다.
애노테이션 처리: 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공합니다.
예를 들어, String.class는 String 클래스에 대한 Class 객체를 나타내며, 이를 통해 String 클래스에 대한 메타데이터를 조회하거나 조작할 수 있습니다.
다음 코드를 실행해봅시다.
package lang.clazz;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
// Class 조회
Class clazz = String.class; // 1. 클래스에서 조회
//Class clazz = new String().getClass(); // 2. 인스턴스에서 조회
//Class clazz = Class.forName("java.lang.String"); // 3. 문자열로 조회
// 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field.getType() + " " + field.getName());
}
// 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
// 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass().getName());
// 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interfaces) {
System.out.println("Interface: = " + i.getName());
}
}
}
class vs clazz - class는 자바의 예약어입니다. 따라서 패키지명, 변수명으로 사용할 수 없습니다.
이런 이유로 자바 개발자들은 class 대신 clazz라는 이름을 관행으로 사용합니다.
clazz는 class와 유사하게 들리고, 이 단어가 class를 의미한다는 것을 쉽게 알 수 있습니다.
주의!
main() 옆에 throws Exception이 추가된 부분에 주의합시다. 이 코드가 없으면 컴파일 오류가 발생합니다.
실행 결과
field = class [B value
...
method = byte[] java.lang.String.value()
method = public boolean java.lang.String.equals(java.lang.Object)
...
Superclass: java.lang.Object
Interface: = java.io.Serializable
Interface: = java.lang.Comparable
...
Class 클래스는 다음과 같이 3가지 방법으로 조회할 수 있습니다.
Class clazz = String.class; // 1. 클래스에서 조회
Class clazz = new String().getClass(); // 2. 인스턴스에서 조회
Class clazz = Clazz.forName("java.lang.String"); // 3. 문자열로 조회
Class 클래스의 주요 기능
getDeclaredFields(): 클래스의 모든 필드를 조회합니다.
getDeclaredMethods(): 클래스의 모든 메서드를 조회합니다.
getSuperclass(): 클래스의 부모 클래스를 조회합니다.
getInterface(): 클래스의 인터페이스들을 조회합니다.
실행 결과를 보면 String 클래스의 다양한 정보를 확인할 수 있습니다.
클래스 생성하기
Class 클래스에는 클래스의 모든 정보가 들어있습니다. 이 정보를 기반으로 인스턴스를 생성하거나, 메서드를 호출하고, 필드의 값도 변경할 수 있습니다.
여기서는 간단하게 인스턴스를 생성해봅시다.
package lang.clazz;
public class ClassCreatMain {
public static void main(String[] args) throws Exception {
//Class helloClass = Hello.class;
Class helloClass = Class.forName("lang.clazz.Hello");
Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance();
String result = hello.hello();
System.out.println("result = " + result);
}
}
실행 결과
result = hello!
getDeclaredConstructor().newInstance()
getDeclaredConstructor() : 생성자를 선택합니다.
newInstance() : 선택된 생성자를 기반으로 인스턴스를 생성합니다.
리플랙션 - reflection
Class를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메소드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메소드를 호출하는 작업을 할 수 있습니다.
이런 작업을 리플렉션이라고 합니다.
추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수도 있습니다.
최신 프레임워크들은 이런 기능을 적극 활용합니다.
지금은 Class가 뭔지, 그리고 대략 어떤 기능들을 제공하는지만 알아두면 충분합니다.
지금은 리플랙션을 학습하는 것 보다 훨씬 더 중요한 기본기들을 학습해야 합니다.
-
☕️[Java] 래퍼 클래스 - 주요 메서드와 성능
래퍼 클래스 - 주요 메서드와 성능.
래퍼 클래스 - 주요 메서드.
래퍼 클래스가 제공하는 주요 메서드를 알아봅시다.
package lang.wrapper;
public class WrapperUtilsMain {
public static void main(String[] args) {
Integer i1 = Integer.valueOf(10); // 숫자, 래퍼 객체 변환.
Integer i2 = Integer.valueOf("10"); // 문자열, 래퍼 객체 변환.
int intValue = Integer.parseInt("10"); // 문자열 전용, 기본형 변환.
// 비교
int compareResult = i1.compareTo(20);
System.out.println("compareResult = " + compareResult);
// 산술 연산
System.out.println("sum: " + Integer.sum(10, 20));
System.out.println("min: " + Integer.min(10, 20));
System.out.println("max: " + Integer.max(10, 20));
}
}
실행 결과
compareResult = -1
sum: 30
min: 10
max: 20
valueOf(): 래퍼 타입을 반환합니다. 숫자, 문자열을 모두 지원합니다.
parseInt(): 문자열을 기본형으로 변환합니다.
compareTo(): 내 값과 인수로 넘어온 값을 비교합니다. 내 값이 크면 1, 같으면 0, 내 값이 작으면 -1을 반환합니다.
Integer.sum(), Integer.min(), Integer.max(): static 메서드 입니다. 간단한 덧셈, 작은 값, 큰 값 연산을 수행합니다.
pareInt() vs valueOf()
원하는 타입에 맞는 메서드를 사용하면 됩니다.
valueOf("10")는 래퍼 타입을 반환합니다.
parseInt("10")는 기본형을 반환합니다.
Long.parseLong() 처럼 각 타입에 parseXxx()가 존재합니다.
래퍼 클래스와 성능
래퍼 클래스는 객체이기 때문에 기본형보다 다양한 기능을 제공합니다.
그렇다면 더 좋은 래퍼 클래스만 제공하면 되지 기본형을 제공하는 이유는 무엇일까요?
다음 코드를 실행해서 기본형과, 래퍼 클래스의 성능 차이를 비교해봅시다.
package lang.wrapper;
public class WrapperVsPrimitive {
public static void main(String[] args) {
int iterations = 1_000_000_000; // 반복 횟수 설정, 10억
long startTime, endTime;
// 기본형 long 사용
long sumPrimitive = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumPrimitive += i;
}
endTime = System.currentTimeMillis();
System.out.println("sumPrimitive = " + sumPrimitive);
System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");
// 래퍼 클래스 Long 사용
Long sumWrapper = 0L;
startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
sumWrapper += i; // 오토 박싱 발생
}
endTime = System.currentTimeMillis();
System.out.println("sumWrapper = " + sumWrapper);
System.out.println("래퍼 클래스 Long 실행 시간: " + (endTime - startTime) + "ms");
}
}
단순히 값을 반복해서 10억 번 더합니다.
기본형 long에 더하는 것과 래퍼 클래스 Long에 더하는 부분으로 나누어 테스트 합니다. 결과 값은 같습니다.
실행 결과 - M1 맥북 기준
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 381ms
sumWrapper = 499999999500000000
래퍼 클래스 Long 실행 시간: 1640ms
기본형 연산이 래퍼 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있습니다. 참고로 계산 결과는 시스템 마다 다릅니다.
기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지합니다. 예를 들어 int 는 보통 4바이트의 메모리를 사용합니다.
래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용합니다. 자바 버전과 시스템마다 다르지만 대략 8-16바이트의 메모리를 추가로 사용합니다.
기본형, 래퍼 클래스 어떤 것을 사용?
이 연산은 10억 번의 연산을 수행했을 때 0.3초와, 1.5초의 차이입니다.
기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘다 매우 빠르게 연산이 수행됩니다.
0.3초 나누기 10억, 1.5초 나누기 10억이다.
일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐입니다.
CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만 ~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려합시다.
그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 됩니다.
유지보수 vs 최적화
유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 합니다.
특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많습니다.
코드 변경 없이 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 합니다. 최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것입니다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있습니다.
특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸립니다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많습니다.
권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것입니다.
-
☕️[Java] 래퍼 클래스 - 기본형의 한계 2
래퍼 클래스 - 기본형의 한계 2
기본형과 null
기본형은 항상 값을 가져야 합니다.
하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있습니다.
다음 코드를 작성해봅시다.
package lang.wrapper;
public class MyIntegerNullMain0 {
public static void main(String[] args) {
int[] intArr = {-1, 0, 1, 2, 3};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // -1
}
private static int findValue(int[] intArr, int target) {
for (int value : intArr) {
if (value == target) {
return value;
}
}
return -1;
}
}
findValue()는 배열에 찾는 값이 있으면 해당 값을 반환하고, 찾는 값이 없으면 -1을 반환합니다.
findValue()는 결과로 int를 반환합니다.
int와 같은 기본형은 항상 값이 있어야 합니다.
여기서도 값을 반환할 때 값을 찾지 못하면 숫자 중 하나를 반환해야 하는데 보통 -1 또는 0을 사용합니다.
실행 결과
-1
0
1
-1
실행 결과를 보면 입력값이 -1일 때 -1을 반환합니다.
그런데 배열에 없는 값인 100을 입력해도 같은 -1을 반환합니다.
입력값이 -1일 때를 생각해보면, 배열에 -1 값이 있어서 -1을 반환한 것인지, 아니면 -1 값이 없어서 -1을 반환한 것인지 명확하지 않습니다.
객체의 경우 데이터가 없다는 null이라는 명확한 값이 존재합니다.
다음 코드를 작성해봅시다.
package lang.wrapper;
public class MyIntegerNullMain1 {
public static void main(String[] args) {
MyInteger[] intArr = {new MyInteger(-1), new MyInteger(0), new MyInteger(1)};
System.out.println(findValue(intArr, -1)); // -1
System.out.println(findValue(intArr, 0));
System.out.println(findValue(intArr, 1));
System.out.println(findValue(intArr, 100)); // null
}
private static MyInteger findValue(MyInteger[] intArr, int target) {
for (MyInteger myInteger : intArr) {
if (myInteger.getValue() == target) {
return myInteger;
}
}
return null;
}
}
실행 결과
-1
0
1
null
앞서 만든 MyInteger 래퍼 클래서를 사용했습니다.
실행 결과를 보면 -1을 입력했을 때는 -1을 반환합니다.
100을 입력했을 때는 값이 없다는 null을 반환합니다.
기본형은 항상 값이 존재해야 합니다.
숫자의 경우 0, -1 같은 값이라도 항상 존재해야 합니다.
반면에 객체인 참조형은 값이 없다는 null을 사용할 수 있습니다.
물론 null 값을 반환하는 경우 잘못하면 NullPointerException이 발생할 수 있기 때문에 주의해서 사용해야 합니다.
-
-
-
☕️[Java] 래퍼 클래스 - 기본형의 한계 1
래퍼 클래스 - 기본형의 한계 1
기본형의 한계
자바는 객체 지향 언어입니다.
그런데 자바 안에 객체가 아닌 것이 있습니다.
바로 int, double 같은 기본형(Primitive Type)입니다.
기본형은 객체가 아니기 때문에 다음과 같은 한계가 있습니다.
객체가 아님: 기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없습니다. 예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서드를 제공할 수 없습니다.
추가로 객체 참고가 필요한 컬렉션 프레임워크를 사용할 수 없습니다. 그리고 제네릭도 사용할 수 없습니다.
null 값을 가질 수 없음: 기본형 데이터 타입은 null 값을 가질 수 없습니다. 때로는 데이터가 없음 이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없습니다.
기본형의 한계를 이해하기 위해, 두 값을 비교해서 다음과 같은 결과를 출력하는 간단한 코드를 작성해봅시다.
왼쪽의 값이 더 작다 -1
두 값이 같다 0
왼쪽의 값이 더 크다 1
package lang.wrapper;
public class MyIntegerMethodMain0 {
public static void main(String[] args) {
int value = 10;
int i1 = compareTo(value, 5);
int i2 = compareTo(value, 10);
int i3 = compareTo(value, 20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
public static int compareTo(int value, int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
여기서는 value와 비교 대상 값을 compareTo()라는 외부 메서드를 사용해서 비교합니다.
그런데 자기 자신인 value와 다른 값을 연산하는 것이기 때문에 항상 자기 자신의 값인 value가 사용됩니다.
이런 경우 만약 value가 객체라면 value 객체 스스로 가지 자신의 값과 다른 값을 비교하는 메서드를 만드는 것이 더 유용할 것입니다.
직접 만든 래퍼 클래스.
int를 클래스로 만들어 봅시다.
int는 클래스가 아니지만, int 값을 가지고 클래스를 만들면 됩니다.
다음 코드는 마치 int를 클래스로 감싸서 만드는 것 처럼 보입니다.
이렇게 특정 기본형을 감싸서(Wrap) 만드는 클래스를 래퍼 클래스(Wrapper class)라 합니다.
package lang.wrapper;
public class MyInteger {
private final int value;
public MyInteger(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public int compareTo(int target) {
if (value < target) {
return -1;
} else if (value > target) {
return 1;
} else {
return 0;
}
}
@Override
public String toString() {
return String.valueOf(value);
}
}
MyInteger는 int value라는 단순한 기본형 변수를 하나 가지고 있습니다.
그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공합니다.
앞에서 본 compareTo() 메서드를 클래스 내부로 캡슐화 했습니다.
이 클래스는 불변으로 설계했습니다.
MyInteger 클래스는 단순한 데이터 조각인 int를 내부에 품고, 메서드를 통해 다양한 기능을 추가했습니다.
덕분에 데이터 조각에 불과한 int를 MyInteger를 통해 객체로 다룰 수 있게 되었습니다.
package lang.wrapper;
public class MyIntegerMethodMain1 {
public static void main(String[] args) {
MyInteger myInteger = new MyInteger(10);
int i1 = myInteger.compareTo(5);
int i2 = myInteger.compareTo(10);
int i3 = myInteger.compareTo(20);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
}
실행 결과
i1 = 1
i2 = 0
i3 = -1
myInteger.compareTo()는 자기 자신의 값을 외부의 값과 비교합니다.
MyInteger는 객체이므로 자신이 가진 메서드를 편리하게 호출할 수 있습니다.
참고로 int는 기본형이기 때문에 스스로 메서드를 가질 수 없습니다.
-
☕️[Java] String 클래스 - 정리
String 클래스 - 정리
1. 자바에서 문자를 다루는 대표적인 타입
char
String
char 배열을 사용하면 문자열을 다룰 수 있으나 불편합니다.
그래서 String이라는 것을 Java에서 제공해줍니다.
2. String 클래스를 통해 문자열을 생성하는 2가지 방법.
String str1 = "hello"; // 방법 1 -> 문자열 리터럴
String str2 = new String("hello"); // 방법 2
쌍따옴표 사용: 방법 1
객체 생성 : 방법 2
문자열 리터럴을 사용하더라도 결국 new String("hello"); 가 되는 것 입니다.
편의상 쌍따옴표로 문자열을 감싸면(문자열 리터럴) 자바 언어에서 new String("hello"); 와 같이 변경해 줍니다.
이 경우 실제로는 성정 최적화를 위해 문자열 풀을 사용합니다.
문자열은 참조형입니다.
3. String 클래스 내부
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
3. String 클래스 비교.
String 클래스를 비교할 때는 == 비교가 아니라 항상 equals() 비교를 해야합니다.
동일성(Identity) : == 연산자를 사용해서 두 객체의 참조(Reference) 가 동일한 객체를 가리키고 있는지 확인합니다.
동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인합니다.
4. 문자열 리터럴, 문자열 풀.
String str3 = "hello" 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용합니다.
자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어 둡니다.
이때 같은 문자열이 있으면 만들지 않습니다.
String str3 - "hello"와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello"라는 문자를 가진 String 인스턴스를 찾습니다.
그리고 찾은 인스턴스의 참조(x003)를 반환합니다.
String str4 = "hello"의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3과 같은 x003 참조를 사용합니다.
문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있습니다.
따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공합니다.
참고
풀(Pool)은 자원이 모여있는 곳을 의미합니다.
프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻합니다.
여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적입니다.
대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화할 수 있습니다.
참고로 문자열 풀은 힙 영역을 사용합니다.
그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있습니다.
5. String 클래스는 불변객체.
String은 불변 객체입니다.
따라서 생성 이후에 절대로 내부의 문자열 값을 변경할 수 없습니다.
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1.concat(" java");
System.out.println("str1 = " + str1);
System.out.println("str1 = " + str2);
}
String은 불변 객체입니다. 따라서 변경이 필요한 경우 기존 값을 변경하지 않고, 대신에 새로운 결과를 만들어서 반환합니다.
실행 결과
str1 = hello
str2 = hello java
String.concat()은 내부에서 새로운 String 객체를 만들어서 반환합니다.
따라서 불변과 기존 객체의 값을 유지합니다.
문자열에서 무언가 변경하는 것이 있으면 그 내부에서 인스턴스를 생성하여 반환합니다.
때문에 반환값을 받아서 사용해야 합니다.
6. String 클래스가 불변으로 설계된 이유.
사이드 이팩트 문제.
문자열 풀을 사용시에 불변으로 설계되어 있어야 안전하게 사용할 수 있기 때문입니다.
7. String 클래스 주요 메서드.
주요 메서드 블로그 글 1
주요 메서드 블로그 글 2
참고. CharSequence
CharSequence 는 String, StringBuilder의 상위 타입입니다.
문자열을 처리하는 다양한 객체를 받을 수 있습니다.
8. StringBuilder - 가변 String
불변인 String 클래스의 단점: 뭔가 값을 더하거나 변경하거나 할 때마다 계속 새로운 객체를 만들어내야 한다는 점 입니다. 때문에 성능도 느려질 수 있습니다. (물론, 불변이기 때문에 안전하기는 합니다.)
이러한 단점들 때문에 StringBuilder 가 있습니다.
StringBuilder: 가변 String 입니다.
값을 쭉 바꾸고 쓰면 되는데, 마지막에는 다시 String으로 바꾸는 toString()을 사용합니다.
즉, 다시 안전한 불변으로 바꾸는 작업입니다.
변경이 필요할 때 가변으로, 마지막에는 불변으로 바꿔서 쓰는것을 권장합니다.
“뭔가 문자열을 변경할 일이 많다” -> StringBuilder 를 사용하면 됩니다.
9. String 최적화.
그러나 생각보다 StringBuilder를 사용할 때가 많이 없습니다.
그 이유는 자바가 String을 최적화하기 때문입니다.
컴파일러에서도 최적화를 하고, 변수로 되어있어도 컴파일러가 최적화를 수행합니다.
예를 들어 Java가 직접 StringBuilder를 사용합니다.
String result = new StringBuilder().append(str1).append(str2).toString();
10. String 최적화가 어려운 경우.
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대략 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 1000000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder 는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다.
이럴 때는 직접 StringBuilder 를 사용하면 됩니다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
String result = sb.toString();
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
11. 메서드 체이닝.
자기 자신의 값을 반환해서 메서드를 쭉 연결해서 사용할 수 있습니다.
자바의 많은 라이브러리들이 메서드 체이닝 기법을 사용하고 있습니다.
메서드 체이닝 블로그 글
-
☕️[Java] String 최적화
String 최적화.
자바의 String 최적화
자바 컴파일러는 다음과 같이 문자열 리터럴을 더하는 부분을 자동으로 합쳐줍니다.
문자열 리터럴 최적화
컴파일 전
String helloWorld = "Hello, " + "World!";
컴파일 후
String helloWorld = "Hello, World!";
따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상됩니다.
String 변수 최적화
문자열 변수의 경우 그 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없기 때문에 단순하게 합칠 수 없습니다.
String result = str1 + str2;
이런 경우 예를 들면 다음과 같이 최적화를 수행합니다.(최적화 방식은 자바 버전에 따라 달라집니다.)
String result = new StringBuilder().append(str1).append(str2).toString();
참고: 자바 9부터는 StringConcatFactory를 사용해서 최적화를 수행합니다.
이렇듯 자바가 최적화를 처리해주기 때문에 지금처럼 간단한 경우에는 StringBuilder를 사용하지 않아도 됩니다.
대신에 문자열을 더하기 연산(+)을 사용하면 충분합니다.
String 최적화가 어려운 경우
다음과 같이 문자열을 루프안에서 문자열을 더하는 경우에는 최적화가 이루어지지 않습니다.
package lang.string.builder;
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for (int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
왜냐하면 대락 다음과 같이 최적화가 되기 때문입니다.(최적화 방식은 자바 버전에 따라 다릅니다.)
String result = "";
for (int i = 0; i < 100000; i++) {
result = new StringBuilder().append(result).append("Hello Java ").toString();
}
반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 합니다.
반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정됩니다.
이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없습니다.
따라서, 이런 상황에서는 최적화가 어렵습니다.
StringBuilder는 물론이고, 아마도 대략 반복 횟수인 100,000번의 String 객체를 생성했을 것입니다.
실행 결과
result = Hello Java Hello Java ...
time = 2528ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 2.5초가 걸렸습니다.
이럴 때는 직접 StringBuilder를 사용하면 됩니다.
package lang.string.builder;
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
실행 결과
result = Hello Java Hello Java ...
time = 4ms
1000ms = 1초
M1 맥북을 기준으로 100000회 더했을 때 약 0.004초가 걸렸습니다.
정리
문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 됩니다.
StringBuilder를 직접 사용하는 것이 더 좋은 경우
반복문에서 반복해서 문자를 연결할 때
조건문을 통해 동적으로 문자열을 조합할 때
복잡한 문자열의 특정 부분을 변경해야 할 때
매우 긴 대용량 문자열을 다룰 때
참고: StringBuilder vs StringBuffer
StringBuilder와 똑같은 기능을 수행하는 StringBuffer 클래스도 있습니다.
StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느립니다.
StringBuilder는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠릅니다.
-
☕️[Java] 메서드 체이닝 - Method Chaining
메서드 체이닝 - Method Chaining.
간단한 예제 코드로 메서드 체이닝(Method Chaining)에 대해서 알아봅시다.
package lang.string.chaining;
public class ValueAdder {
private int value;
public ValueAdder add(int addValue) {
value += addValue;
return this;
}
public int getValue() {
return value;
}
}
단순히 값을 누적해서 더하는 기능을 제공하는 클래스입니다.
add() 메서드를 호출할 때 마다 내부의 value에 값을 누적합니다.
add() 메서드를 보면 자기 자신(this)의 참조값을 반환합니다. 이 부분을 유의해서 봅시다.
package lang.string.chaining;
public class MethodChainingMain1 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
adder.add(1);
adder.add(2);
adder.add(3);
int result = adder.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
add() 메서드를 여러번 호출해서 값을 누적해서 더하고 출력합니다.
여기서는 add() 메서드의 반환값은 사용하지 않았습니다.
이번에는 add() 메서드의 반환값을 사용해봅시다.
package lang.string.chaining;
public class MethodChainingMain2 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
ValueAdder adder1 = adder.add(1);
ValueAdder adder2 = adder1.add(2);
ValueAdder adder3 = adder2.add(3);
int result = adder3.getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 결과는 기존과 같습니다.
adder.add(1)을 호출합니다.
add() 메서드는 결과를 누적하고 자기 자신의 참조값인 this(x001)를 반환합니다.
adder1 변수는 adder와 같은 x001 인스턴스를 참조합니다.
add()메서드는 자기 자신(this) 메서드의 참조값을 반환합니다.
이 반환값을 adder1, adder2, adder3에 보관했습니다.
따라서 adder, adder1, adder2, adder3은 모두 같은 참조값을 사용합니다.
왜냐하면 add() 메서드가 자기 자신(this)의 참조값을 반환했기 때문입니다.
그런데 이 방식은 처음 방식보다 더 불편하고, 코드도 더 잘 읽히지 않습니다.
이런 방식을 왜 사용하는 것 일까요?
이번에는 방금 사용했던 방식에서 반환된 참조값을 새로운 변수에 담아서 보관하지 않고, 대신에 바로 메서드 호출에 사용해봅시다.
package lang.string.chaining;
public class MethodChainingMain3 {
public static void main(String[] args) {
ValueAdder adder = new ValueAdder();
int result = adder.add(1).add(2).add(3).getValue();
System.out.println("result = " + result);
}
}
실행 결과
result = 6
실행 순서
add() 메서드를 호출하면 ValueAdder 인스턴스 자신의 참조값(x001)이 반환됩니다.
이 반환된 참조값을 변수에 담아두지 않아도 됩니다.
대신에 반환된 참조값을 즉시 사용해서 바로 메서드를 호출할 수 있습니다.
다음과 같은 순서로 실행됩니다.
adder.add(1).add(2).add(3).getValue(); // value = 0
x001.add(1).add(2).add(3).getValue(); // value = 0, x001.add(1)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(2).add(3).getValue(); // value = 1, x001.add(2)을 호출하면 그 결과로 x001을 반환합니다.
x001.add(3).getValue(); // value = 3, x001.add(3)을 호출하면 그 결과로 x001을 반환합니다.
x001.getValue(); // value = 6
6
메서드 호출의 결과로 자기 자신의 참조값을 반환하면, 반환된 참조값을 사용해서 메서드 호출을 계속 이어갈 수 있습니다.
코드를 보면 .을 찍고 메서드를 계속 연결해서 사용합니다.
마치 메서드가 체인으로 연결된 것 처럼 보입니다.
이러한 기법을 메서드 체이닝이라고 합니다.
물론 실행 결과도 기존과 동일합니다.
기존에는 메서드를 호출할 때 마다 계속 변수명에 .을 찍어야 했습니다. 예) adder.add(1), adder.add(2)
매서드 체이닝 방식은 메서드가 끝나는 시점에 바로 .을 찍어서 변수명을 생략할 수 있습니다.
메서드 체이닝이 가능한 이유는 자기 자신의 참조값을 반환하기 때문입니다.
이 참조값에 .을 찍어서 바로 자신의 메서드를 호출할 수 있습니다.
메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어줍니다.
StringBuilder와 메서드 체인(Chain)
StringBuilder는 메서드 체이닝 기법을 제공합니다.
StringBuilder의 append() 메서드를 보면 자기 자신의 참조값을 반환합니다.
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder에서 문자열을 변경하는 대부분의 메서드도 메서드 체이닝 기법을 제공하기 위해 자기 자신을 반환합니다. 예) insert(), delete(), reverse()
앞서 StringBuilder를 사용한 코드는 다음과 같이 개선할 수 있습니다.
package lang.string.builder;
public class StringBuilderMain1_2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
String string = sb.append("A").append("B").append("C").append("D")
.insert(4, "Java")
.delete(4,8)
.reverse()
.toString();
System.out.println("string = " + string);
}
}
실행 결과
string = DCBA
정리
“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다” 는 말이 있습니다.
메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리합니다.
참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용합니다.
-
☕️[Java] String 클래스 - 주요 메서드 2
String 클래스 - 주요 메서드 2
문자열 조작 및 변환
substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다.
replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다.
replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다.
replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다.
toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다.
trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다.
strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11
package lang.string.method;
public class StringChangeMain2 {
public static void main(String[] args) {
String strWithSpaces = " Java Programming ";
System.out.println("소문자로 변환: " + strWithSpaces.toLowerCase());
System.out.println("대문자로 변환: " + strWithSpaces.toUpperCase());
System.out.println("공백 제거(trim): '" + strWithSpaces.trim() + "'");
System.out.println("공백 제거(strip): '" + strWithSpaces.strip() + "'");
System.out.println("앞 공백 제거(stripLeading): '" + strWithSpaces.stripLeading() + "'");
System.out.println("뒤 공백 제거(stripTrailing): '" + strWithSpaces.stripTrailing() + "'");
}
}
실행 결과
소문자로 변환: java programming
대문자로 변환: JAVA PROGRAMMING
공백 제거(trim): 'Java Programming'
공백 제거(strip): 'Java Programming'
앞 공백 제거(stripLeading): 'Java Programming '
뒤 공백 제거(stripTrailing): ' Java Programming'
문자열 분할 및 조합
split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다.
join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다.
package lang.string.method;
public class StringSplitJoinMain {
public static void main(String[] args) {
String str = "Apple,Banana,Orange";
// split()
String[] splitStr = str.split(",");
for (String s : splitStr) {
System.out.println(s);
}
String joinStr = "";
for (int i = 0; i < splitStr.length; i++) {
String string = splitStr[i];
joinStr += string;
if (i != splitStr.length-1) {
joinStr += "-";
}
}
System.out.println("joinStr = " + joinStr);
// join()
String joinedStr = String.join("-", "A", "B", "C");
System.out.println("연결된 문자열 = " + joinedStr);
// 문자열 배열 연결
String result = String.join("-", splitStr);
System.out.println("result = " + result);
}
}
실행 결과
Apple
Banana
Orange
joinStr = Apple-Banana-Orange
연결된 문자열 = A-B-C
result = Apple-Banana-Orange
기타 유틸리티.
valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다.
toCharArray() : 문자열을 문자 배열로 변환합니다.
format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다.
matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다.
package lang.string.method;
public class StringUtilsMain1 {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
Object obj = new Object();
String str = "Hello, Java!";
// valueOf 메서드
String numString = String.valueOf(num);
System.out.println("숫자의 문자열 값: " + numString);
String boolString = String.valueOf(bool);
System.out.println("불리언의 문자열 값: " + boolString);
String objString = String.valueOf(obj);
System.out.println("객체의 문자열 값: " + objString);
// 문자 + x -> 문자
String numString2 = "" + num;
System.out.println("빈 문자열 + num: " + numString2);
// toCharArray 메서드
char[] strCharArray = str.toCharArray();
System.out.println("문자열을 문자 배열로 변환 : " + strCharArray);
for (char c : strCharArray) {
System.out.print(c);
}
System.out.println();
}
}
실행 결과
숫자의 문자열 값: 100
불리언의 문자열 값: true
객체의 문자열 값: java.lang.Object@a09ee92
빈 문자열 + num: 100
문자열을 문자 배열로 변환 : [C@30f39991
Hello, Java!
package lang.string.method;
public class StringUtilsMain2 {
public static void main(String[] args) {
int num = 100;
boolean bool = true;
String str = "Hello, Java!";
// format 메서드
String format1 = String.format("num: %d, bool: %b, str: %s", num, bool, str);
System.out.println(format1);
String format2 = String.format("숫자: %.2f ", 10.1234);
System.out.println(format2);
//printf
System.out.printf("숫자: %.2f\n", 10.1234);
// matches 메서드
String regex = "Hello, (Java!|World)";
System.out.println("'str'이 패턴과 일치하는가? " + str.matches(regex));
}
}
실행 결과
num: 100, bool: true, str: Hello, Java!
숫자: 10.12
숫자: 10.12
'str'이 패턴과 일치하는가? true
format 메서드에서 %d는 숫자 %b는 boolean, %s는 문자열을 뜻합니다.
-
☕️[Java] StringBuilder - 가변 String
StringBuilder - 가변 String.
불변인 String 클래스의 단점.
불변인 String 클래스에도 단점이 있습니다.
다음 예를 봅시다.
참고로 실제로 작동하는 코드는 아닙니다.
두 문자를 더하는 경우 다음과 같이 작동합니다.
"A" + "B";
String("A") + String("B"); // 문자는 String 타입.
String("A").concat(String("B")); // 문자의 더하기는 concat을 사용.
new String("AB") // String은 불변입니다. 따라서 새로운 객체가 생성됩니다.
불변인 String의 내부 값은 변경할 수 없습니다.
따라서 변경된 값을 기반으로 새로운 String 객체를 생성합니다.
더 많은 문자를 더하는 경우를 살펴봅시다.
String str = "A" + "B" + "C" + "D";
String str = String("A") + String("B") + String("C") + String("D");
String str = new String("AB") + String("C") + String("D");
String str = new String("ABC") + String("D");
String str = new String("ABCD");
이 경우 총 3개의 String 클래스가 추가로 생성됩니다.
그런데 문제는 중간에 만들어진 new String("AB"), new String("ABC")는 사용되지 않습니다. 최종적으로 만들어진 new String("ABCD")만 사용됩니다.
결과적으로 중간에 만들어진 new String("AB)", new String("ABC")는 제대로 사용되지도 않고, 이후 GC의 대상이 됩니다.
불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점입니다.
문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 합니다.
결과적으로 컴퓨터의 CPU, 메모리 자원을 더 많이 사용하게 됩니다.
그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모합니다.
참고 : 실제로는 문자열을 다룰 때 자바가 내부에서 최적화를 적용합니다.
StringBuilder
이 문제를 해결하는 방법은 단순합니다.
바로 불변이 아닌 가변 String이 존재하면 됩니다.
가변은 내부의 값을 바로 변경하면 되기 때문에 새로운 객체를 생성할 필요가 없습니다.
따라서 성능과 메모리 사용면에서 불변보다 더 효율적입니다.
이런 문제를 해결하기 위해 자바는 StringBuilder라는 가변 String을 제공합니다.
물론 가변의 경우 사이드 이펙트에 주의해서 사용해야 합니다.
StringBuilder는 내부에 final이 아닌 변경할 수 있는 byte[]을 가지고 있습니다.
public final class StringBuilder {
char[] value; // 자바 9 이전
char[] value; // 자바 9 이후
// 여러 메서드
public StringBuilder append(String str) {...}
public int length() {...}
...
}
(실제로는 상속 관계에 있고 부모 클래스인 AbstractStringBuilder에 value 속성과 length() 메서드가 있습니다.)
StringBuilder 사용 예
실제 StringBuilder를 어떻게 사용하는지 확인해 봅시다.
package lang.string.builder;
public class StringBuilderMain1_1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("A");
sb.append("B");
sb.append("C");
sb.append("D");
System.out.println("sb = " + sb);
sb.insert(4, "Java");
System.out.println("sb = " + sb);
sb.delete(4,8);
System.out.println("delete = " + sb);
sb.reverse();
System.out.println("reverse = " + sb);
// StringBuilder(가변) -> String(불변)으로 바꿀 수 있음.
String string = sb.toString();
System.out.println("string = " + string);
}
}
StringBuilder 객체를 생성합니다.
append() 메서드를 사용해 여러 문자열을 추가합니다.
insert() 메서드로 특정 위치에 문자열을 삽입합니다.
delete() 메서드로 특정 범위의 문자열을 삭제합니다.
reverse() 메서드로 문자열을 뒤집습니다.
마지막으로 toString() 메소드를 사용해 StringBuilder의 결과를 기반으로 String을 생성해서 반환합니다.
실행 결과
sb = ABCD
sb = ABCDJava
delete = ABCD
reverse = DCBA
string = DCBA
가변(Mutable) vs 불변(Immutable)
String은 불변합니다. 즉, 한 번 생성되면 그 내용을 변경할 수 없습니다.
따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려집니다.
이 과정에서 메모리와 처리 시간을 더 많이 소모합니다.
반면에, StringBuilder는 가변적입니다.
하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않습니다.
이로 인해 메모리 사용을 줄이고 성늘을 향상시킬 수 있습니다. 단 사이드 이펙트를 주의해야 합니다.
StringBuilder는 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한(불변) String으로 변환하는 것이 좋습니다.
-
☕️[Java] String 클래스 - 주요 메서드 1
String 클래스 - 주요 메서드 1
주요 메서드 목록
String 클래스는 문자열을 편리하게 다루기 위한 다양한 메서드를 제공합니다.
여기서는 자주 사용하는 기능 위주로 나열했습니다.
참고로 기능이 너무 많기 때문에 메서드를 외우기 보다는 주로 사용하는 메서드가 이런 것이 있구나 대략 알아두고, 필요할 때 검색하거나 API 문서를 통해서 원하는 기능을 찾는 것이 좋습니다.
문자열 정보 조회
length() : 문자열의 길이를 반환합니다.
isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0).
isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다.
문자열 비교
equals(Object anObject) : 두 문자열이 동일한지 비교합니다.
equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다.
compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다.
compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다.
startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다.
endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다.
문자열 검색
contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다.
indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다.
lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다.
문자열 조작 및 변환
substring(int beginIndex) / substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
concat(String str) : 문자열의 끝에 다른 문자열을 붙입니다.
replace(CharSequence target, CharSequence replacement) : 특정 문자열을 새 문자열로 대체합니다.
replaceAll(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체합니다.
replaceFirst(String regex, String replacement) : 문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체합니다.
toLowerCase() / toUpperCase() : 문자열을 소문자나 대문자로 변환합니다.
trim() : 문자열 양쪽 끝의 공백을 제거합니다. 단순 Whitespace만 제거할 수 있습니다.
strip() : Whitespace 와 유니코드 공백을 포함해서 제거합니다, 자바 11
문자열 분할 및 조합.
split(String regex) : 문자열을 정규 표현식을 기준으로 분할합니다.
join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합합니다.
기타 유틸리티.
valueOf(Object obj) : 다양한 타입을 문자열로 변환합니다.
toCharArray() : 문자열을 문자 배열로 변환합니다.
format(String format, Object... args) : 형식 문자열과 인자를 사용하여 새로운 문자열을 생성합니다.
matches(String regex) : 문자열이 주어진 정규 표현식과 일치하는지 확인합니다.
이제 본격적으로 하나씩 알아봅시다.
참고: CharSequence 는 String, StringBuilder의 상위 타입입니다.
문자열을 처리하는 다양한 객체를 받을 수 있습니다.
문자열 정보 조회
length() : 문자열의 길이를 반환합니다.
isEmpty() : 문자열이 비어 있는지 확인합니다. (길이가 0).
isBlank() : 문자열이 비어 있는지 확인합니다. (길이가 0이거나 공백(Whitespace)만 있는 경우), 자바 11
charAt(int index): 지정된 인덱스에 있는 문자를 반환합니다.
package lang.string.method;
public class StringInfoMain {
public static void main(String[] args) {
String str = "Hello, Java!";
System.out.println("문자열의 길이: " + str.length());
System.out.println("문자열이 비어 있는지: " + str.isEmpty());
System.out.println("문자열이 비어 있거나 공백인지 1: " + str.isBlank());
System.out.println("문자열이 비어 있거나 공백인지 2: " + " ".isBlank());
char c = str.charAt(7);
System.out.println("7번째 인덱스의 문자 = " + c);
}
}
실행 결과
문자열의 길이: 12
문자열이 비어 있는지: false
문자열이 비어 있거나 공백인지 1: false
문자열이 비어 있거나 공백인지 2: true
7번째 인덱스의 문자 = J
문자열 비교
equals(Object anObject) : 두 문자열이 동일한지 비교합니다.
equalsIgnoreCase(String anotherString) : 두 문자열을 대소문자 구분 없이 비교합니다.
compareTo(String anotherString) : 두 문자열을 사전 순으로 비교합니다.
compareToIgnoreCase(String str) : 두 문자열을 대소문자 구분 없이 사전적으로 비교합니다.
startWith(String prefix) : 문자열이 특정 접두사로 시작하는지 확인합니다.
endWith(String suffix) : 문자열이 특정 접미사로 끝나는지 확인합니다.
package lang.string.method;
public class StringComparisonMain {
public static void main(String[] args) {
String str1 = "Hello, Java!"; // 대문자 일부 있음
String str2 = "hello, java!";
String str3 = "Hello, World!";
System.out.println("str equals str2: " + str1.equals(str2));
System.out.println("str equalsIgnoreCase str2: " + str1.equalsIgnoreCase(str2));
System.out.println("'a' compareTo 'b': " + "a".compareTo("b"));
System.out.println("'b' compareTo 'a': " + "b".compareTo("a"));
System.out.println("'c' compareTo 'a': " + "c".compareTo("a"));
System.out.println("str1 compareTo str3: " + str1.compareTo(str3));
System.out.println("str1 compareToIgnoreCase str2: " + str1.compareToIgnoreCase(str2));
System.out.println("str1 starts with 'Hello': " + str1.startsWith("Hello"));
System.out.println("str1 ends with 'Java!': " + str1.endsWith("Java!"));
}
}
실행 결과
str equals str2: false
str equalsIgnoreCase str2: true
'a' compareTo 'b': -1
'b' compareTo 'a': 1
'c' compareTo 'a': 2
str1 compareTo str3: -13
str1 compareToIgnoreCase str2: 0
str1 starts with 'Hello': true
str1 ends with 'Java!': true
문자열 검색
contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인합니다.
indexOf(String ch) / indexOf(String ch, int fromIndex): 문자열이 처음 등장하는 위치를 반환합니다.
lastIndexOf(String ch) : 문자열이 마지막으로 등장하는 위치를 반환합니다.
package lang.string.method;
public class StringSearchMain {
public static void main(String[] args) {
String str = "Hello, Java! Welcome to Java world.";
System.out.println("문자열에 'Java'가 포함되어 있는지: " + str.contains("Java"));
System.out.println("'Java'의 첫 번째 인덱스: " + str.indexOf("Java"));
System.out.println("인덱스 10부터 'Java'의 인덱스: " + str.indexOf("Java", 10));
System.out.println("'Java'의 마지막 인덱스: " + str.lastIndexOf("Java"));
}
}
실행 결과
문자열에 'Java'가 포함되어 있는지: true
'Java'의 첫 번째 인덱스: 7
인덱스 10부터 'Java'의 인덱스: 24
'Java'의 마지막 인덱스: 24
-
-
-
☕️[Java] String 클래스 - 기본
String 클래스 - 기본.
자바에서 문자를 다루는 대표적인 타입은 char, String 2가지가 있습니다.
package lang.string;
public class CharArrayMain {
public static void main(String[] args) {
char[] charArr = new char[]{'h', 'e', 'l', 'l', 'o'};
System.out.println(charArr);
String str = "hello";
System.out.println("str = " + str);
}
}
실행 결과
hello
str = hello
기본형인 char는 문자 하나를 다룰 때 사용합니다.
char를 사용해서 여러 문자를 나열하려면 char[]을 사용해야 합니다.
하지만 이렇게 char[]을 직접 다루는 방법은 매우 불편하기 때문에 자바는 문자열을 매우 편리하게 다룰 수 있는 String 클래스를 제공합니다.
String 클래스를 통해 문자열을 생성하는 방법은 2가지가 있습니다.
package lang.string;
public class StringBasicMain {
public static void main(String[] args) {
String str1 = "hello";
String str2 = new String("hello");
System.out.println("str1 = " + str1);
System.out.println("str2 = " + str2);
}
}
쌍따옴표: "hello"
객체 생성: new String("hello");
String은 클래스입니다.
int, boolean 같은 기본형이 아니라 참조형입니다.
따라서 str1 변수에는 String 인스턴스의 참조값만 들어갈 수 있습니다.
따라서 다음 코드는 뭔가 어색합니다.
String str1 = "hello";
문자열은 매우 자주 사용됩니다.
그래서 편의상 쌍따옴표로 문자열을 감싸면 자바 언어에서 new String("hello")와 같이 변경해 줍니다.(이 경우 실제로는 성능 최적화를 위해 문자열 풀을 사용합니다.)
String str1 = "hello"; // 기존
String str1 = new String("hello"); // 변경
String 클래스 구조
String 클래스는 대략 다음과 같이 생겼습니다.
public final class String {
// 문자열 보관
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
// 여러 메서드
public String concat(String str) {...}
public int length() {...}
}
클래스이므로 속성과 기능을 가집니다.
속성(필드)
private final char[] value;
여기에는 String의 실제 문자열 값이 보관됩니다.
문자 데이터 자체는 char[] 에 보관됩니다.
String 클래스는 개발자가 직접 다루기 불편한 char[]을 내부에 감추고 String 클래스를 사용하는 개발자가 편리하게 문자열을 다룰 수 있도록 다양한 기능을 제공합니다.
그리고 메서드 제공을 넘어서 자바 언어 차원에서도 여러 편의 문법을 제공합니다.
참고: 자바 9 이후 String 클래스 변경 사항
자바 9부터 String 클래스에서 char[] 대신에 byte[]을 사용합니다.
private final byte[] value;
자바에서 문자 하나를 표현하는 char는 2byte를 차지합니다.
그런데 영어, 숫자는 보통 1byte로 표현이 가능합니다.
그래서 단순 영어, 숫자로 표현된 경우 1byte를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용)
그렇지 않은 나머지의 경우 2byte인 UTF-16 인코딩을 사용합니다.
따라서 메모리를 더 효율적으로 사용할 수 있게 변경되었습니다.
기능(메서드)
String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공합니다.
기능이 방대하므로 필요한 기능이 있으면 검색하거나 API 문서를 찾아봅시다.
주요 메서드는 다음과 같습니다.
length() : 문자열의 길이를 반환합니다.
charAt(inte index) : 특정 인덱스의 문자를 반환합니다.
substring(int beinIndex, int endIndex) : 문자열의 부분 문자열을 반환합니다.
indexOF(String str) : 특정 문자열이 시작되는 인덱스를 반환합니다.
toLowerCase(), toUpperCase() : 문자열을 소문자 또는 대문자로 변환합니다.
trim() : 문자열 양 끝의 공백을 제거합니다.
concat(String str) : 문자열을 더합니다.
String 클래스와 참조형
String은 클래스입니다.
따라서 기본형이 아니라 참조형입니다.
참조형은 변수에 계산할 수 있는 값들이 들어있는 것이 아니라 x001과 같이 계산할 수 없는 참조값이 들어있습니다.
따라서 원칙적으로 + 같은 연산을 사용할 수 없습니다.
```java
package lang.string;
public class StringConcatMain {
public static void main(String[] args) {
String a = “hello”;
String b = “ jave”;
String result1 = a.concat(b);
String result2 = a + b;
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2); } } ``` - 자바에서 문자열을 더할 때는 `String`이 제공하는 `concat()`과 같은 메서드를 사용해야 합니다.
- 하지만 문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 `+` 연산을 제공합니다.
실행 결과
result1 = hello jave
result2 = hello jave
-
-
-
☕️[Java] 불변 객체 - 예제
불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해봅시다.
앞의 Address, ImmutableAddress를 그래로 활용합니다.
변경 클래스 사용
package lang.immutable.address;
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A", address);
MemberV1 memberB = new MemberV1("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원 B의 주소를 부산으로 변경해야함
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원A와 회원B는 둘다 서울에 살고 있습니다.
중간에 회원B의 주소를 부산으로 변경해야 합니다.
그런데 회원A와 회원B는 같은 Address 인스턴스를 참조하고 있습니다.
회원B의 주소를 부산으로 변경하는 순간 회원A의 주소도 부산으로 변경됩니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='부산'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
package lang.immutable.address;
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
//memberB.getAddress().setValue("부산"); // 컴파일 오류
memberB.setAddress(new ImmutableAddress("부산"));
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
회원B의 주소를 중간에 부산으로 변경하려고 시도합니다.
하지만 ImmutableAddress에는 값을 변경할 수 있는 메서드가 없습니다.
따라서 컴파일 오류가 발생합니다.
결국 memberB.setAddress(new ImmutableAddress("부산"))와 같이 새로운 주소 객체를 만들어서 전달합니다.
실행 결과
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = MemberV1{name='회원A', address=Address{value='서울'}}
memberB = MemberV1{name='회원B', address=Address{value='부산'}}
사이드 이펙트가 발생하지 않습니다. 회원A는 기존 주소를 그대로 유지합니다.
-
☕️[Java] 공유 참조와 사이드 이펙트
공유 참조와 사이드 이펙트.
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말합니다.
앞서 b의 값을 부산으로 변경한 코드를 다시 분석해 봅시다.
b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
개발자는 b의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했습니다.
하지만 a, b는 같은 인스턴스를 참조합니다. 따라서 a의 값도 함께 부산으로 변경되어 버립니다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라고 합니다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생합니다.
이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있습니다.
사이드 이펙트 해결방안
생각해보면 문제의 해결방안은 아주 단순합니다.
다음과 같이 a와 b가 처음부터 서로 다른 인스턴스를 참조하면 됩니다.
Address a = new Address("서울");
Address b = new Address("서울");
코드를 작성해봅시다.
package lang.immutable.address;
public class RefMain1_2 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
Address a = new Address("서울");
Address b = new Address("서울");
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 b의 주소값만 부산으로 변경된 것을 확인할 수 있습니다.
a와 b는 서로 다른 Address 인스턴스를 참조합니다.
a와 b는 서로 다른 인스턴스를 참조합니다.
따라서 b가 참조하는 인스턴스의 값을 변경해도 a에는 영향을 주지 않습니다.
여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다
지금까지 발생한 모든 문제는 같은 객체(인스턴스)를 변수 a, b가 함께 공유하기 때문에 발생했습니다.
따라서 객체를 공유하지 않으면 문제가 해결됩니다.
여기서 변수 a,b가 서로 각각 다른 주소지로 변경할 수 있어야 합니다.
이렇게 하려면 서로 다른 객체를 참조하면 됩니다.
객체를 공유
Address a = new Address("서울");
Address b = a;
이 경우 a, b 둘 다 같은 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 불가능합니다.
객체를 공유 하지 않음
Address a = new Address("서울");
Address b = new Address("서울");
이 경우 a, b는 서로 다른 Address 인스턴스를 바라보기 때문에 한쪽의 주소만 부산으로 변경하는 것이 가능합니다.
이처럼 단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결됩니다.
쉽게 이야기해서 여러 변수가 하나의 객체를 공유하지 않으면 지금까지 설명한 문제들이 발생하지 않습니다.
그런데 여기에 문제가 있습니다.
하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것입니다
다음 예시를 봅시다.
참조값의 공유를 막을 수 있는 방법이 없습니다.
Address a = new Address("서울");
Address b = a; // 참조값 대입을 막을 수 있는 방법이 없습니다.
b = a와 같은 코드를 작성하지 않도록 해서, 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같습니다.
하지만 Address를 사용하는 개발자 입장에서 실수로 b = a라고 해도 아무런 오류가 발생하지 않습니다.
왜냐하면 자바 문법상 Address b = a와 같은 참조형 변수의 대입은 아무런 문제가 없기 때문입니다.
다음과 같이 새로운 객체를 참조형 변수에 대입하든, 또는 기존 객체를 참조형 변수에 대입하든, 다음 두 코드 모두 자바 문법상 정상인 코드입니다.
Address b = new Address("서울"); // 새로운 객체 참조
Address b = a // 기존 객체 공유 참조
참조값을 다른 변수에 대입하는 순간 여러 변수가 하나의 객체를 공유하게 됩니다.
쉽게 이야기해서 객체의 공유를 막을 수 있는 방법이 없습니다!
기본형은 항상 값을 복사해서 대입하기 때문에 값이 절대로 공유되지 않습니다.
하지만 참조형의 경우 참조값을 복사해서 대입하기 때문에 여러 변수에서 얼마든지 같은 객체를 공유할 수 있습니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있습니다.
물론 개발자가 눈을 크게 잘 뜨고! 집중해서 코드를 잘 작성하면서 사이드 이펙트 문제를 일으키지 않을 수 있습니다.
하지만 실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생합니다.
다음 코드를 봅시다.
package lang.immutable.address;
public class RefMain1_3 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
Address a = new Address("서울");
Address b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
change(b, "부산");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
private static void change(Address address, String changeAddress) {
System.out.println("주소 값을 변경합니다 -> " + changeAddress);
address.setValue(changeAddress);
}
}
앞서 작성한 코드와 같은 코드입니다.
단순히 change() 메서드만 하나 추가되었습니다.
그리고 change() 메서드에서 Address 인스턴스에 있는 value 값을 변경합니다.
main() 메서드만 보면 a의 값이 함께 부산으로 변경된 이류를 찾기가 더 어렵습니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
주소 값을 변경합니다 -> 부산
a = Address{value='부산'}
b = Address{value='부산'}
여러 변수가 하나의 객체를 참조하는 공유 참조를 막을 수 있는 방법은 없습니다.
그럼 공유 참조로 인해 발생하는 문제를 어떻게 해결할 수 있을까요?
단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성해야 할까요?
-
☕️[Java] 불변 객체 - 도입
불변 객체 - 도입
지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제입니다.
하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없습니다.
그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아닙니다.
객체를 공유한다고 바로 사이드 이펙트가 발생하지 않습니다.
문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있습니다.
앞의 예를 떠올려보면 a, b는 처음 시점에는 둘 다 "서울"이라는 주소를 사용해야 합니다.
그리고 이후에 b의 주소를 "부산"으로 변경해야 합니다.
Address a = new Address("서울");
Address b = a;
따라서 처음에는 b = a와 같이 "서울"이라는 Address 인스턴스를 a, b가 함께 사용하는 것이, 다음 코드와 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적입니다.
인스턴스가 하나이니 메모리가 절약되고, 인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적입니다.
Address a = new Address("서울");
Address b = new Address("서울");
여기까지는 Address b = a와 같이 공유 참조를 사용해도 아무런 문제가 없습니다. 오히려 더 효율적입니다.
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생합니다.
b.setValue("부산"); // b의 값을 부산으로 변경해야 합니다.
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없습니다.
기본형과 다르게 참조형인 객체는 처음부터 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었습니다.
따라서 이것은 문제가 아닙니다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 더이선가 변경했기 때문입니다.
만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것입니다.
불변 객체 도입
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 합니다.
앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어 봅시다.
package lang.immutable.address;
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
내부 값이 변경되면 안됩니다.
따라서 value의 필드를 final로 선언했습니다.
값을 변경할 수 있는 setValue()를 제거했습니다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능합니다.
불변 클래스를 만드는 방법은 아주 단순합니다.
어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 됩니다.
package lang.immutable.address;
public class RefMain2 {
public static void main(String[] args) {
// 참조형 변수는 하나의 인스턴스를 공유할 수 있습니다.
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a; // 참조값 대입을 막을 수 있는 방법이 없다.
System.out.println("a = " + a);
System.out.println("b = " + b);
// b.setValue("부산"); // 컴파일 오류 발생
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
}
}
ImmutableAddress의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었습니다.
이제 ImmutableAddress 인스턴스의 값을 변경할 수 있는 방법은 없습니다.
ImmutableAddress를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫습니다.
예를 들어 b.setValue("부산")을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지한다.
따라서 어쩔 수 없이 새로운 ImmutableAddress("부산") 인스턴스를 생성해서 b에 대입한다.
결과적으로 a, b는 서로 다른 인스턴스를 참조하고, a가 참조하던 ImmutableAddress는 그대로 유지됩니다.
실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 a의 값은 그대로 유지되는 것을 확인할 수 있습니다.
자바에서 객체의 공유 참조는 막을 수 없습니다.
ImmutableAddress는 불변 객체입니다. 따라서 값을 변경할 수 없습니다.
ImmutableAddress은 불변 객체이므로 b가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 합니다.
정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있습니다.
객체의 공유 참조는 막을 수 없습니다.
그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생합니다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어 사용하면 됩니다.
불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단됩니다.
불변 객체는 값을 변경할 수 없습니다.
따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 합니다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않습니다.
참고 - 가변(Mutable) 객체 VS 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻입니다.)
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻입니다.(사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻입니다.)
Address 는 가변 클래스입니다. 이 클래스로 객체를 생성하면 가변 객체가 됩니다.
ImmutableAddress는 불변 클래스입니다. 이 클래스로 객체를 생성하면 불변 객체가 됩니다.
-
-
-
-
☕️[Java] Object와 OCP
Object와 OCP.
만약 Object가 없고, 또 Object가 제공하는 toString()이 없다면 서로 아무 관계가 없는 객체의 정보를 출력하기 어려울 것입니다.
여기서 아무 관계가 없다는 것은 공통의 부모가 없다는 뜻 입니다.
아마도 다음의 BadObjectPrinter 클래스와 같이 각각의 클래스마다 별도의 메서드를 작성해야 할 것입니다.
BadObjectPrinter
public class BadObjectPrinter {
public static void print(Car car) { // Car 전용 메서드
String string = "객체 정보 출력: " + car.carInfo(); // carInfo() 메서드 만듬
System.out.println(string);
}
public static void print(Dog dog) { // Dog 전용 메서드
String string = "객체 정보 출력: " + dog.dogInfo(); // dogInfo() 메서드 만듬
System.out.println(string);
}
}
구체적인 것에 의존
BadObjectPrinter는 구체적인 타입인 Car, Dog를 사용합니다.
따라서 이후에 출력해야 할 구체적인 클래스가 10개로 늘어나면 구체적인 클래스에 맞추어 메서드도 10개로 계속 늘어나게 됩니다.
이렇게 BadObjectPrinter 클래스가 구체적인 특정 클래스인 Car, Dog를 사용하는 것을 BadObjectPrinter는 Car, Dog에 의존한다고 표현합니다.
다행히도 자바에는 객체의 정보를 사용할 때, 다형적 참조 문제를 해결해줄 Object 클래스와 메서드 오버라이딩 문제를 해결해줄 Object.toString() 메서드가 있습니다.(물론 직접 Object와 비슷한 공통의 부모 클래스를 만들어서 해결할 수도 있습니다.)
추상적인 것에 의존
앞서 만든 ObjectPrinter 클래스는 Car, Dog 같은 구체적인 클래스를 사용하는 것이 아니라, 추상적인 Object 클래스를 사용합니다.
이렇게 ObjectPrinter 클래스가 Object 클래스를 사용하는 것을 Object에 클래스에 의존한다고 표현합니다.
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
ObjectPrinter는 구체적인 것에 의존하는 것이 아니라 추상적인 것에 의존합니다.
추상적 : 여기서 말하는 추상적이라는 뜻은 단순히 추상 클래스나 인터페이스만 뜻하는 것은 아닙니다.
Animal과 Dog, Cat의 관계를 떠올려봅시다.
Animal 같은 부모 타입으로 올라갈 수록 개념은 더 추상적이게 되고, Dog, Cat과 같이 하위 타입으로
내려갈 수록 개념은 더 구체적이게 됩니다.
ObjectPrinter와 Object를 사용하는 구조는 다형성을 매우 잘 활용하고 있습니다.
다형성을 잘 활용한다는 것은 다형적 참조와 메서드 오버라이딩을 적절하게 사용한다는 뜻입니다.
ObjectPrinter의 print() 메서드와 전체 구조를 분석해봅시다.
다형적 참조 : print(Object obj), Object 타입을 매개변수로 사용해서 다형적 참조를 사용합니다. Car, Dog 인스턴스를 포함한 세상의 모든 객체 인스턴스를 인수로 받을 수 있습니다.
메서드 오버라이딩 : Object는 모든 클래스의 부모입니다. 따라서 Dog, Car와 같은 구체적인 클래스는 Object가 가지고 있는 toString() 메서드를 오버라이딩 할 수 있습니다.
따라서 print(Object obj) 메서드는 Dog, Car와 같은 구체적인 타입에 의존(사용)하지 않고, 추상적인 Object 타입에 의존하면서 런타임에 각 인스턴스의 toString()을 호출할 수 있습니다.
OCP 원칙
OCP 원칙을 떠올려 봅시다.
Open : 새로운 클래스를 추가하고, toString()을 오버라이딩해서 기능을 확장할 수 있습니다.
Closed : 새로운 클래스를 추가해도 Object와 toString()을 사용하는 클라이언트 코드인 ObjectPrinter는 변경하지 않아도 됩니다.
다형적 참조, 메서드 오버라이딩, 그리고 클라이언트 코드가 구체적인 Car, Dog에 의존하는 것이 아니라 추상적인 Object에 의존하면서 OCP 원칙을 지킬 수 있었습니다.
덕분에 새로운 클래스를 추가하고 toString() 메서드를 새롭게 오버라이딩해서 기능을 확장할 수 있습니다.
그리고 이러한 변화에도 불구하고 클라이언트 코드인 ObjectPrinter는 변경할 필요가 없습니다.
ObjectPrinter는 모든 타입의 부모인 Object를 사용하고, Object가 제공하는 toString() 메서드만 사용합니다.
따라서 ObjectPrinter를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다.
System.out.println()
지금까지 설명한 ObjectPrinter.print()는 사실 System.out.println()의 작동 방식을 설명하기 위해 만든 것입니다.
System.out.println() 메서드도 Object 매개변수를 사용하고 내부에서 toString()을 호출합니다.
따라서 System.out.println()를 사용하면 세상의 모든 객체의 정보(toString())를 편리하게 출력할 수 있습니다.
자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용합니다.
지금까지 배운 toString() 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 어버라이딩해서 사용할 수 있도록 설계되어 있습니다.
참고 - 정적 의존관계 vs 동적 의존관계
정적 의존관계는 컴파일 시간에 결정되며, 주로 클래스 간의 관계를 의미합니다. 앞서 보여준 클래스 의존 관계 그림이 바로 정적 의존관계입니다.
쉽게 이야기해서 프로그램을 실행하지 않고, 클래스 내에서 사용하는 타입들만 보면 쉽게 의존관계를 파악할 수 있습니다.
동적 의존관계는 프로그램을 실행하는 런타임에 확인할 수 있는 의존관계입니다. 앞서 ObjectPrinter.print(Object obj)에 인자로 어떤 객체가 전달 될 지는 프로그램을 실행해봐야 알 수 있습니다.
어떤 경우에는 Car 인스턴스가 넘어오고, 어떤 경우에는 Dog 인스턴스가 넘어옵니다. 이렇게 런타임에 어떤 인스턴스를 사용하는지를 나타내는 것이 동적 의존관계입니다.
참고로 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존관계를 뜻합니다.
예) ObjectPrinter는 Object에 의존합니다.
-
☕️[Java] toString()
toString()
Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공합니다.
그래서 디버깅과 로깅에 유용하게 사용됩니다.
이 메서드는 Object 클래스에 정의되므로 모든 클래스에서 상속받아 사용할 수 있습니다.
코드로 확인해봅시다.
package lang.object.tostring;
public class ToStringMain1 {
public static void main(String[] args) {
Object object = new Object();
String string = object.toString();
//toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
}
}
실행 결과
java.lang.Object@a09ee92
java.lang.Object@a09ee92
Object.toString()
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Object가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공합니다
println()과 toString()
그런데 toString()의 결과를 출력한 코드와 object를 println()에 직접 출력한 코드의 결과가 완전히 같습니다.
String string = object.toString();
//toString() 반환값 출력
System.out.println(string);
// object 직접 출력
System.out.println(object);
System.out.println() 메서드는 사실 내부에서 toString()을 호출합니다.
Object 타입(자식 포함)이 println()에 인수로 전달되면 내부에서 obj.toString() 메서드를 호출해서 결과를 출력합니다.
public void println(Object x) {
String s = String.valueOf(x);
// ...
}
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
따라서 println()을 사용할 때, toString()을 직접 호출할 필요 없이 객체를 바로 전달하면 객체의 정보를 출력할 수 있습니다.
toString() 오버라이딩.
Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못합니다.
그래서 보통 toString()을 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적입니다.
package lang.object.tostring;
public class Car {
private String carName;
public Car(String carName) {
this.carName = carName;
}
}
package lang.object.tostring;
public class Dog {
private String dogName;
private int age;
public Dog(String dogName, int age) {
this.dogName = dogName;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"dogName='" + dogName + '\'' +
", age=" + age +
'}';
}
}
package lang.object.tostring;
public class ObjectPrinter {
public static void print(Object obj) {
String string = "객체 정보 출력: " + obj.toString();
System.out.println(string);
}
}
package lang.object.tostring;
public class ToStringMain2 {
public static void main(String[] args) {
Car car = new Car("Model Y");
Dog dog1 = new Dog("멍멍이1", 2);
Dog dog2 = new Dog("멍멍이2", 5);
System.out.println("1. 단순 toString 호출");
System.out.println(car.toString());
System.out.println(dog1.toString());
System.out.println(dog2.toString());
System.out.println("2. println 내부에서 toString 호출");
System.out.println(car);
System.out.println(dog1);
System.out.println(dog2);
System.out.println("3. Object 다용성 활용");
ObjectPrinter.print(car);
ObjectPrinter.print(dog1);
ObjectPrinter.print(dog2);
}
}
실행 결과
1. 단순 toString 호출
lang.object.tostring.Car@452b3a41
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@452b3a41
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
3. Object 다용성 활용
객체 정보 출력: lang.object.tostring.Car@452b3a41
객체 정보 출력: Dog{dogName='멍멍이1', age=2}
객체 정보 출력: Dog{dogName='멍멍이2', age=5}
Car 인스턴스는 toString()을 재정의 하지 않습니다.
따라서 Object가 제공하는 기본 toString() 메서드를 사용합니다.
Dog 인스턴스는 toString()을 재정의 한 덕분에 객체의 상태를 명확하게 확인할 수 있습니다.
Object obj의 인수로 car(Car)가 전달됩니다.
메서드 내부에서 obj.toString()을 호출합니다.
obj는 Object 타입입니다.
따라서 Object에 있는 toString()을 찾습니다.
이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. 재정의된 메서드가 없습니다.
Object.toString()을 실행합니다.
ObjectPrinter.print(dog) // main에서 호출
void print(Object obj = dog(Dog)) { // 인수 전달
String string = "객체 정보 출력: " + obj.toString();
}
Object obj의 인수로 dog(Dog) 가 전달 됩니다.
메서드 내부에서 obj.toString()을 호출합니다.
obj는 Object 타입입니다.
따라서 Object에 있는 toString()을 찾습니다.
이때 자식에 재정의(오버라이딩)된 메서드가 있는지 찾아봅니다. Dog에 재정의된 메서드가 있습니다.
Dog.toString()을 실행합니다.
참고 - 객체의 참조값 직접 출력
toString()은 기본으로 객체의 참조값을 출력합니다.
그런데 toString()이나 hashCode()를 재정의하면 객체의 참조값을 출력할 수 없습니다.
이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있습니다.
String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue)
실행 결과
refValue = 72ea2f77
-
-
-
-
-
-
-
-
☕️[Java] 좋은 객체 지향 프로그래밍이란?
좋은 객체 지향 프로그래밍이란?
객체 지향 특징
추상화
캡슐화
상속
다형성
객체 지향 프로그래밍?
객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 “객체” 들의 모임으로 파악하고자 하는 것입니다.
각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있습니다.(협력)
객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용됩니다.
유연하고, 변경에 용이?
레고 블럭 조립하듯이
키보드, 마우스 갈아 끼우듯이
컴퓨터 부품 갈아 끼우듯이
컴포넌트를 쉽고 유연하게 변경하면서 개발할 수 있는 방법
다형성(Polymorphism)
다형성의 실세계 비유
실세계와 객체 지향을 1:1로 매칭 X
그래도 실세계의 비유로 이해하기에는 좋음
역할과 구현으로 세상을 구분
운전자 - 자동차
공연무대
로미오와 줄리렛 공연
예시
운전자 - 자동차
공연 무대
키보드, 마우스, 세상의 표준 인터페이스들
정렬 알고리즘
할인 정책 로직
역할과 구현을 분리
역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해집니다.
장점
클라이언트는 대상의 역할(인터페이스)만 알면 됩니다.
클라이언트는 구현 대상의 내부 구조를 몰라도 됩니다.
클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않습니다.
클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않습니다.
역할과 구현을 분리 2
자바 언어
자바 언어의 다형성을 활용합니다.
역할 = 인터페이스.
구현 = 인터페이스를 구현한 클래스, 구현 객체.
객체를 설계할 때 역할과 구현을 명확히 분리합니다.
객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듭니다.
객체의 협력이라는 관계부터 생각
혼자 있는 객체는 없습니다.
클라이언트: 요청, 서버 응답
수 많은 객체 클라이언트와 객체 서버는 서로 협력 관계를 가집니다.
자바 언어의 다형성
오버라이딩을 떠올려봅시다.
오버라이딩은 자바 기본 문법입니다.
오버라이딩 된 메서드가 실행합니다.
다형성으로 인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있습니다.
물론 클래스 상속 관계도 다형성, 오버라이딩 적용 가능합니다.
다형성의 본질
인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있습니다.
다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야 합니다.
클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있습니다.
역할과 구현을 분리 3
정리
실세계의 역할과 구현이라는 편리한 컨셉을 다형성을 통해 객체 세상으로 가져올 수 있습니다.
유연하고, 변경이 용이합니다.
확장 가능한 설계입니다.
클라이언트에 영향을 주지 않는 변경이 가능합니다.
인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.
한계
역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생합니다.
자동차를 비행기로 변경해야 한다면?
대본 자체가 변경된다면?
USB 인터페이스가 변경된다면?
인터페이스를 안정적으로 잘 설계하는 것이 중요합니다.
정리
다형성이 가장 중요합니다!
디자인 패턴 대부분은 다형성을 활용하는 것입니다.
스프링의 핵심인 제어의 역전(IoC), 의존관계 주입(DI)도 결국 다형성을 활용하는 것입니다.
다형성을 잘 활용하면 마치 레고 블럭 조립하듯이! 공연 무대의 배우를 선택하듯이! 구현을 편리하게 변경할 수 있습니다.
-
-
-
-
☕️[Java] 인터페이스
인터페이스
“자바는 순수 추상 클래스를 더 편리하게 사용할 수 있는 인터페이스라는 기능을 제공합니다.”
순수 추상 클래스
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
“인터페이스는 class 가 아니라 interface 키워드를 사용하면 됩니다.”
인터페이스
public interface InterfaceAnimal {
public abstract void sound();
public abstract void move();
}
인터페이스 - public abstract 키워드 생략 가능
public interface InterfaceAnimal {
void sound();
void move();
}
순수 추상 클래스는 다음과 같은 특징을 갖습니다.
인스턴스를 생성할 수 없습니다.
상속시 모든 메서드를 오버라이딩 해야 합니다.
주로 다형성을 위해 사용됩니다.
인터페이스는 앞서 설명한 순수 추상 클래스와 같습니다. 여기에 약간의 편의 기능이 추가 됩니다.
인터페이스 메서드는 모두 public, abstract입니다.
메서드에 public abstract를 생략할 수 있습니다.(참고로 생략이 권장됩니다.)
인터페이스는 다중 구현(다중 상속)을 지원합니다.
인터페이스와 멤버 변수
public interface InterfaceAnimal {
public static final int MY_PI = 3.14;
}
인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주됩니다.
final은 변수의 값을 한 번 설정하면 수정할 수 없다는 뜻입니다.
자바에서 static final을 사용해 정적이면서 고칠 수 없는 변수를 상수라 하고, 관례상 상수는 대문자에 언더스코어(_)로 구분합니다.
해당 키워드는 다음과 같이 생략할 수 있습니다.(생략이 권장됩니다.)
public interface InterfaceAnimal {
int MY_PI = 3.14;
}
예제 5
클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속)관계는 UML에서 점선을 사용합니다.
package poly.ex5;
public class InterfaceMain {
public static void main(String[] args) {
// 인터페이스 생성 불가
// InterfaceAnimal interfaceAnimal = new InterfaceAnimal();
Cat cat = new Cat();
Dog dog = new Dog();
Caw caw = new Caw();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(caw);
}
// 변하지 않는 부분
private static void soundAnimal(InterfaceAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
앞서 설명한 순수 추상 클래스 예제와 거의 유사합니다.
순수 추상 클래스가 인터페이스가 되었을 뿐입니다.
클래스, 추상 클래스, 인터페이스는 모두 똑같습니다.
클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같습니다.
모두 자바에서는 .class로 로 다루어집니다.
인터페이스를 작성할 때도 .java에 인터페이스를 정의합니다.
인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 됩니다.
상속 vs 구현
부모 클래스의 기능을 자식 클래스가 상속 받을 때, 클래스는 상속 받는다고 표현하지만, 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현합니다.
이렇게 서로 다르게 표현하는 이유를 알아봅시다.
상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적입니다.
하지만 인터페이스는 모든 메서드가 추상 메서드입니다.
따라서 물려 받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 합니다.
따라서 구현한다고 표현합니다.
인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 합니다.
따라서 인터페이스의 경우 상속이 아니라 해당 인터페이스를 구현한다고 표현합니다.
상속과 구현은 사람이 표현하는 단어만 다를 뿐이지 자바 입장에서는 똑같습니다. 일반 상속 구조와 동일하게 작동합니다.
인터페이스를 사용해야 하는 이유.
모든 메서드가 추상 메서드인 경우 순수 추상 클래스로 만들어도 되고, 인터페이스를 만들어도 됩니다.
그런데 왜 인터페이스를 사용해야 할까요? 단순히 편리하다는 이유를 넘어서 다음과 같은 이유가 있습니다.
제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 규약(제약)을 주는 것입니다.
USB 인터페이스를 생각해봅시다. USB 인터페이스에 맞추어 키보드, 마우스를 개발하고 연결해야 합니다. 그렇지 않으면 작동하지 않습니다.
인터페이스의 규약(제약)은 반드시 구현해야 하는 것입니다.
그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있습니다.
이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 됩니다.
인터페이스는 모든 메서드가 추상 메서드입니다. 따라서 이런 문제를 원천 차단할 수 있습니다.
다중 구현 : 자바에서 클래스 상속은 부모를 하나만 지정할 수 있습니다.
반면에 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능합니다.
좋은 프로그램은 제약이 있는 프로그램입니다.
참고
자바 8에 등장한 default 메서드를 사용하면 인터페이스도 메서드를 구현할 수 있습니다.
하지만 이것은 예외적으로 아주 특별한 경우에만 사용해야 합니다.
자바 9에서 등장한 인터페이스의 private 메서드도 마찬가지입니다.
지금 학습 단계에서는 이 부분들을 고려하지 않는 것이 좋습니다.
이 부분은 추후에 따로 학습하고 정리할 것 입니다.
-
-
-
☕️[Java] 다형성 활용3
다형성 활용 3
이번에는 배열과 for문을 사용해서 중복을 제거해보겠습니다.
package poly.ex2;
public class AnimalPolyMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
Animal[] animalArr = {dog, cat, caw};
// 변하지 않는 부분
for (Animal animal : animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
배열은 같은 타입의 데이터를 나열할 수 있습니다.
Dog, Cat, Caw는 모두 Animal의 자식이므로 Animal 타입입니다.
Animal 타입의 배열을 만들고 다형적 참조를 사용하면 됩니다.
// 둘은 같은 코드입니다.
Animal[] animalArr = new Anima[]{dog, cat, caw};
Animal[] animalArr = {dog, cat, caw};
다형적 참조 덕분에 Dog, Cat, Caw의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했습니다.
이제 배열을 for문을 사용해서 반복하면 됩니다.
// 변하지 않는 부분
for (Animal animal: animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
animal.sound()를 호출하지만 배열에는 Dog, Cat, Caw의 인스턴스가 들어있습니다.
메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound() 메서드가 호출됩니다.
조금 더 개선
package poly.ex2;
public class AnimalPolyMain3 {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Cat()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
// 변하지 않는 부분
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
Animal[] animalArr를 통해서 배열을 사용합니다.
soundAnimal(Animal animal)
하나의 동물을 받아서 로직을 처리합니다.
새로운 동물이 추가되어도 soundAnimal(...) 메서드는 코드 변경 없이 유지할 수 있습니다.
이렇게 할 수 있는 이유는 이 메서드는 Dog, Cat, Caw 같은 구체적인 클래스를 참조하는 것이 아니라 Animal이라는 추상적인 부모를 참조하기 때문입니다.
따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는 병경 없이 유지할 수 있습니다.
여기서 잘 보면 새로운 동물이 추가되었을 때 코드가 변하는 부분과 변하지 않는 부분이 있습니다.
main()은 코드가 변하는 부분입니다.
새로운 동물을 생성하고 필요한 메서드를 호출합니다.
soundAnimal(...)는 코드가 변하지 않는 부분입니다.
“새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드입니다.”
이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋습니다.
남은 문제
지금까지 설명한 코드에는 사실 2가지 문제가 있습니다.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스는 동물이라는 클래스입니다.
이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까요?
Animal animal = new Animal();
개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상합니다.
이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없습니다.
하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없습니다.
누군가 실수로 new Animal()을 사용해서 Animal의 인스턴스를 생성할 수 있다는 것 입니다.
이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않습니다.
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성.
예를들어서 Animal을 상속 받은 Pig 클래스를 만든다고 가정해봅시다.
우리가 기대하는 것은 Pig 클래스가 sound() 메서드를 오버라이딩 해서 “꿀꿀”이라는 소리가 나도록 하는 것입니다.
그런데 개발자가 실수로 sound() 메서드를 오버라이딩 하는 것을 빠트릴 수 있습니다.
이렇게 되면 부모의 기능을 상속받습니다.
따라서 코드상 아무런 문제가 발생하지 않습니다.
물론 프로그램을 실행하면 기대와 다르게 “꿀꿀”이 아니라 부모 클래스에 있는 Animal.sound()가 호출될 것입니다.
좋은 프로그램은 제약이 있는 프로그램입니다.
추상 클래스와 추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있습니다.
-
-
☕️[Java] 다형성 활용1
다형성 활용1
지금까지 학습한 다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보겠습니다.
아주 단순하고 전통적인 동물 소리 문제로 접근해보겠습니다.
개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해봅시다.
먼저 다형성을 사용하지 않고 코드를 작성해봅시다.
package poly.ex1;
public class Dog {
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex1;
public class Cat {
public void sound() {
System.out.println("야옹");
}
}
package poly.ex1;
public class Caw {
public void sound() {
System.out.println("음메");
}
}
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음메
동물 소리 테스트 종료
단순히 개, 고양이, 소 동물들의 울음 소리를 출력하는 프로그램입니다.
만약 여기서 새로운 동물이 추가되면 어떻게 될까요?
만약 기존 코드에 소가 없다고 가정해봅시다.
소가 추가된다고 가정하면 Caw 클래스를 만들고 다음 코드도 추가해야 합니다.
// Caw를 생성하는 코드
Caw caw = new Caw();
// Caw를 사용하는 코드
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
Caw를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog, Cat, Caw를 사용해서 출력하는 부분은 계속 중복이 증가합니다.
중복 코드
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for문을 사용하면 됩니다.
그런데 Dog, Cat, Caw는 서로 완전히 다른 클래스입니다.
중복 제거 시도
메서드로 중복 제거 시도
메서드를 사용하면 다음과 같이 매개변수의 클래스를 Caw, Dog, Cat 중에 하나로 정해야 합니다.
private static void soundCaw(Caw caw) {
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
따라서 이 메서드는 Caw 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없습니다.
Dog, Cat, Caw의 타입(클래스)이 서로 다르기 때문에 soundCaw 메서드를 함께 사용하는 것은 불가능합니다.
배열과 for문을 통한 중복 제거 시도
Caw[] cawArr = { cat, dog, caw }; // 컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
for (Caw caw : cawArr) {
cawArr.sound()
}
System.out.println("동물 소리 테스트 종료");
배열과 for문 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog, Cat, Caw 중에 하나로 지정해야 합니다.
같은 Caw들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog, Cat, Caw을 하나의 배열에 담는 것은 불가능합니다.
결과적으로 지금 상황에서는 해결방법이 없습니다.
새로운 동물이 추가될 때 마다 더 많은 중복 코드를 작성해야 합니다.
지금까지 설명한 모든 중복 제거 시도가 Dog, Cat, Caw의 타입이 서로 다르기 때문에 불가능합니다.
“문제의 핵심은 바로 타입이 다르다는 점” 입니다.
반대로 이야기하면 Dog, Cat, Caw가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것입니다.
다형성의 핵심은 다형적 참조와 메서드 오버라이딩입니다.
이 둘을 활용하면 Dog, Cat, Caw 가 모두 같은 타입을 사용하고, 각자 자신의 메서드로 호출할 수 있습니다.
-
-
-
-
☕️[Java] 다운캐스팅과 주의점
다운캐스팅과 주의점.
다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있습니다.
다음 코드를 통해 다운캐스팅에서 발생할 수 있는 문제를 확인해봅시다.
package poly.basic;
// 다운캐스팅을 자동으로 하지 않는 이유
public class CastingMain4 {
public static void main(String[] args) {
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); // 문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
child2.childMethod(); // 실행 불가
}
}
실행 결과
Child.childMethod
Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child (poly.basic.Parent and poly.basic.Child are in unnamed module of loader 'app')
at poly.basic.CastingMain4.main(CastingMain4.java:12)
실행 결과를 보면 child1.childMethod()는 잘 호출되었지만, child2.childMethod()는 실행되지 못하고, 그 전에 오류가 발생합니다.
예제의 parent1의 경우 다운캐스팅을 해도 문제가 되지 않습니다.
예제의 parent2를 다운캐스팅하면 ClassCastException 이라는 심각한 런타임 오류가 발생합니다.
이 코드를 자세히 알아봅시다.
Parent parent2 = new Parent()
먼저 new Parent()로 부모 타입으로 객체를 생성합니다.
따라서 메모리 상에 자식 타입은 전혀 존재하지 않습니다.
생성 결과를 parent2에 담아둡니다.
이 경우 같은 타입이므로 여기서는 문제가 발생하지 않습니다.
Child child2 = (Child) parent2
다음으로 parent2를 Child 타입으로 다운캐스팅합니다.
그런데 parent2는 Parent로 생성되었습니다.
따라서 메모리상에 Child 자체가 존재하지 않습니다. Child 자체를 사용할 수 없는 것입니다.
자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastExecption이라는 예외를 발생시킵니다.
예외가 발생하면 다음 동작이 실행되지 않고, 프로그램이 종료됩니다.
따라서 child2.childMethod() 코드 자체가 실행되지 않습니다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅의 경우 이런 문제가 절대로 발생하지 않습니다.
왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성되기 때문입니다!
따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전합니다.
따라서 캐스팅을 생략할 수 있습니다.
반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있습니다.
왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않습니다.
따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 합니다.
클래스 A, B, C는 상속 관계입니다.
new C()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B, C가 모두 생성됩니다.
따라서 C의 부모 타입인 A, B, C 모두 C 인스턴스를 참조할 수 있습니다.
“상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.”
A a = new C(): A로 업캐스팅
B b = new C(): B로 업캐스팅
C c = new C(): 자신과 같은 타입
new B()로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B가 생성됩니다.
따라서 B의 부모 타입인 A, B 모두 B 인스턴스를 참조 할 수 있습니다.
상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않습니다.
하지만 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운캐스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있습니다.
A a = new B() : A로 업캐스팅
B b = new B() : 자신과 같은 타입
C c = new B() : 하위 타입은 대입할 수 없음, 컴파일 오류
C c = (C) new B() : 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C와 관련된 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생
컴파일 오류 vs 런타임 오류
컴파일 오류는 변수명 오류, 잘못된 클래스 이름 사용등 자바 프로그램을 실행하기 전에 발생하는 오류입니다.
이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류 입니다.
반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류입니다.
런타임 오류는 매우 안좋은 오류입니다.
왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문입니다.
-
-
-
-
-
-
☕️[Java] 상속과 접근 제어
상속과 접근 제어.
상속 관계와 접근 제어에 대해 알아보겠습니다.
참고로 접근 제어를 자세히 설명하기 위해 부모와 자식의 패키지를 따로 분리하였습니다.
접근 제어자를 표현하기 위해 UML 표기법을 일부 사용했습니다.
+: public
#: protected
~: default
-: private
접근 제어자를 잠시 복습해봅시다.
접근 제어자의 종류
private: 모든 외부 호출을 막습니다.
default(package-private): 같은 패키지 안에서 호출은 허용합니다.
protected: 같은 패키지 안에서 호출은 허용합니다.
패키지가 달라도 상속 관계의 호출은 허용합니다.
public: 모든 외부 호출을 허용합니다.
순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용합니다.
private -> default -> protected -> public
그림과 같이 다양한 접근 제어자를 사용하도록 코드를 작성해보겠습니다.
Parent
package extends1.access.parent;
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() {
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectedValue);
System.out.println("defaultValue = " + defaultValue);
System.out.println("privateValue = " + privateValue);
// 부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
Child
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
// defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
// privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
// defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
// privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
ExtendsAccessMain
package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
Parent와 Child의 패키지가 다르다는 부분에 유의합시다.
자식 클래스인 Child에서 부모 클래스인 Parent에 얼마나 접근할 수 있는지 확인해봅시다.
publicValue = 1;: 부모의 public 필드에 접근합니다. public이므로 접근할 수 있습니다.
protectedValue = 1;: 부모의 protected 필드에 접근합니다. 자식과 부모는 다른 패키지이지만, 상속 관계이므로 접근할 수 있습니다.
defaultValue = 1;: 부모의 default 필드에 접근합니다. 자식과 부모가 다른 패키지이므로 접근할 수 없습니다.
privateValue = 1;: 부모의 private 필드에 접근합니다. private은 모두 외부 접근을 막으므로 자식이라도 호출할 수 없습니다.
메서드의 경우도 앞서 설명한 필드와 동일합니다.
Child
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectedValue = 1; // 상속 관계 or 같은 패키지
// defaultValue = 1; // 다른 패키지 접근 불가, 컴파일 오류
// privateValue = 1; // 접근 불가, 컴파일 오류
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
// defaultMethod(); // 다른 패키지 접근 불가, 컴파일 오류
// privateMethod(); // 접근 불가, 컴파일 오류
printParent();
}
}
코드를 실행해보면 Child.call() -> Parent.printParent() 순서로 호출합니다.
Child는 부모의 public, protexted 필드나 메서드만 접근할 수 있습니다.
반면에 Parent.printParent()의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있습니다.
접근 제어와 메모리 구조.
본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이때 접근 제어자가 영향을 줍니다.
왜냐하면 객체 내부에서는 자식과 부모가 구분되어 있기 때문입니다.
결국 자식 차입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같습니다.
-
☕️[Java] 상속과 메서드 오버라이딩
상속과 메서드 오버라이딩.
부모 타입의 기능을 자식에서는 다르게 재정의 하고 싶을 수 있습니다.
예를 들어서 자동차의 경우 Car.move()라는 기능이 있습니다.
이 기능을 사용하면 단순히 “차를 이동합니다.” 라고 출력합니다.
전기차의 경우 보통 더 빠르기 때문에 전기차가 move()를 호출한 경우에는 “전기차를 빠르게 이동합니다.”라고 출력을 변경하고 싶습니다.
이렇게 부모에게서 상속 받은 기능을 자식이 재정의 하는 것을 “메서드 오버라이딩(Overriding)” 이라고 합니다.
package extends1.overriding - Car
package extends1.overriding;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
기존 코드와 같습니다.
package extends1.overriding - GasCar
package extends1.overriding;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.overriding - HydrogenCar
package extends1.overriding;
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
package extends1.overriding - ElectricCar
package extends1.overriding;
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
ElectricCar는 부모인 Car의 move() 기능을 그대로 사용하고 싶지 않습니다.
메서드 이름은 같지만 새로운 기능을 사용하고 싶습니다.
그래서 ElectricCar의 move() 메서드를 새로 만들었습니다.
이렇게 부모의 기능을 자식이 새로 재정의하는 것을 “매서드 오버라이딩” 이라고 합니다.
이제 ElectricCar의 move()를 호출하면 Car의 move()가 아니라 ElectricCar의 move()가 호출됩니다.
@Override
@이 붙은 부분을 애노테이션(어노테이션, annotattion)이라 합니다.
애노테이션은 주석과 비슷한데, 프로그램이 읽을 수 있는 특별한 주석이라 생각하면 됩니다.
이 애노테이션은 상위 클래스의 매서드를 오버라이드하는 것임을 나타냅니다.
이름 그대로 오버라이딩한 매서드 위에 이 애노테이션을 붙여야 합니다.
컴파일러는 이 애노테이션을 보고 매서드가 정확히 오버라이드 되었는지 확인합니다.
오바라이딩 조건을 만족시키지 않으면 컴파일 에러를 발생시킵니다.
따라서 실수로 오버라이딩을 못하는 경우를 방지해줍니다.
예를 들어서 이 경우에 만약 부모에 move() 메서드가 없다면 컴파일 오류가 발생합니다.
참고로 이 기능은 필수는 아니지만 코드의 명확성을 위해 붙여주는 것이 좋습니다.
package extends1.overrding - CarMain
package extends1.overriding;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
GasCar gasCar = new GasCar();
gasCar.move();
}
}
실행 결과
전기차를 빠르게 이동합니다.
차를 이동합니다.
실행 결과를 보면 electricCar.move()를 호출했을 때 오버라이딩한 ElectricCar.move() 메서드가 실행된 것을 확인할 수 있습니다.
오버라이딩과 클래스
Car의 move() 메서드를 ElectricCar에서 오버라이딩 했습니다.
오버라이딩과 메모리 구조
electricCar.move()를 호출합니다.
호출한 electricCar의 타입은 ElectricCar입니다. 따라서 인스턴스 내부의 ElectricCar 타입에서 시작합니다.
ElectricCar 타입에 move() 메서드가 있습니다. 해당 메서드를 실행합니다. 이때 실행할 메서드를 이미 찾았으므로 부모 타입을 찾지 않스빈다.
오버로딩(Overloading)과 오버라이딩(Overriding)
메서드 오버로딩: 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러개 정의하는 것을 메서드 오버로딩(Overloading)이라고 합니다.
오버로딩은 번역하면 과적인데, 과하게 물건을 담았다는 뜻입니다.
따라서 같은 이름의 메서드를 여러개 정의했다고 이해하면 됩니다.
메서드 오버라이딩: 메서드 오버라이딩은 하위 클래스에서 상위 클래스의 메서드를 재정의하는 과정을 의미합니다.
따라서 상속 관계에서 사용합니다. 부모의 기능을 자식이 다시 정의하는 것입니다.
오버라이딩을 단순히 해석하면 무언가를 넘어서 타는 것을 말합니다.
자식의 새로운 기능이 부모의 기존 기능을 넘어 타서 기존 기능을 새로운 기능으로 덮어버린다고 이해하면 됩니다.
오버라이딩을 번역하면 무언가를 다시 정의한다고 해서 재정의라 합니다.
상속 관계에서는 기존 기능을 다시 정의한다고 이해하면 됩니다.
실무에서는 메서드 오버라이딩, 메서드 재정의 둘 다 사용합니다.
메서드 오버라이딩 조건
메서드 오버라이딩은 다음과 같은 까다로운 조건을 가지고 있습니다.
다음 내용은 아직 학습하지 않은 내용들도 있으므로 이해하려고 하기 보다는 참고만 합시다.
지금은 단순히 부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분합니다.
메서드 오버라이딩 조건
메서드 이름: 메서드 이름이 같아야 합니다.
메서드 매개변수(파라미터): 매개변수(파라미터) 타입, 순서, 개수가 같아야 합니다.
반환 타입: 반환 타입이 같아야 합니다. 단, 반환 타입이 하위 클래스 타입일 수 있습니다.
접근 제어자: 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안됩니다.
예를 들어, 상위 클래스의 메서드가 protected로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected로 오버라이드 할 수 있지만, private 또는 default로 오버라이드 할 수 없습니다.
예외: 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없습니다.
하지만 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있습니다.
static, final, private: 키워드가 붙은 메서드는 오버라이딩 될 수 없습니다.
static은 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없습니다.
쉽게 이야기해서 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 됩니다.
final 메서드는 재정의를 금지합니다.
private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않습니다.
따라서 오버라이딩 할 수 없습니다.
생성자 오버라이딩: 생성자는 오버라이딩 할 수 없습니다.
-
-
-
-
-
-
-
-
-
-
☕️[Java] static 변수3
static 변수 3
이번에는 static 변수를 정리해보겠습니다.
용어 정리
public class Data3 {
public String name;
public static int count; // static
}
예제 코드에서 name, count는 둘 다 멤버 변수입니다.
멤버 변수(필드)는 static이 붙은 것과 아닌 것에 따라 다음과 같이 분류 할 수 있습니다.
멤버 변수(필드)의 종류.
인스턴스 변수: static이 붙지 않은 멤버 변수, 예) name
static 이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있습니다.
따라서 인스턴스 변수라 합니다.
인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어집니다.
클래스 변수: static이 붙은 멤버 변수, 예) count
클래스 변수, 정적 변수, static 변수등으로 부릅니다. 용어를 모두 사용하니 주의합시다.
static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있습니다.
따라서 클래스 변수라 합니다.
클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어집니다.
인스턴스와는 다른게 보통 여러곳에서 공유하는 목적으로 사용됩니다.
변수와 생명주기
지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관됩니다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거됩니다.
따라서 지역 변수는 생존 주기가 짧습니다.
인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라 합니다. 인스턴스 변수는 힙 영역을 사용합니다.
힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지 생존하기 때문에 보통 지역 변수보다 생존 주기가 깁니다.
클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관 되는 변수입니다.
메서드 영역은 프로그램 전체에서 사용하는 공용 공간입니다.
클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성됩니다.
그리고 JVM이 종료될 때까지 생명주기가 이어집니다.
따라서 가장 긴 생명주기를 가집니다.
static이 정적이라는 이유는 바로 여기에 있습니다.
힙 역역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거됩니다.
반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거됩니다.
정적 변수는 이름 그대로 정적입니다.
정적 변수 접근 법
static 변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있습니다.
DataCountMain3 마지막 코드에 다음 부분을 추가하고 실행해보겠습니다.
DataCountMain3 - 추가
// 추가
// 인스턴스를 통한 접근
Data3 data4 = new Data3("D");
// 클래스를 통합 접근
System.out.println(Data3.count);
실행 결과 - 추가된 부분
4
4
둘 다 차이는 없습니다. 둘다 결과적으로 정적 변수에 접근합니다.
인스턴스를 통한 접근 data4.count
정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않습니다.
왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문입니다.
클래스를 통한 접근 Data3.count
정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확합니다.
따라서 정적 변수에 접근할 때는 클래스를 통해서 접근합시다.
-
-
☕️[Java] static 메서드 2
static 메서드 2
정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있습니다.
하지만 정적 메서드는 언제나 사용할 수 있는 것이 아닙니다.
정적 메서드 사용법
static 메서드는 static만 사용할 수 있습니다.
클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있습니다.
클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없습니다.
반대로 모든 곳에서 static을 호출할 수 있습니다.
정적 메서드는 공용 기능입니다.
따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있습니다.
예제를 통해 정적 메서드의 사용법을 확인해봅시다.
DecoData
package static2;
public class DecoData {
private int instanceValue;
private static int staticValue;
public static void staticCall() {
//instanceValue++; // 인스턴스 변수 접근, compile error
//instanceMethod(); // 인스턴스 메서드 접근, compile error
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 접근
}
public void instanceCall() {
instanceValue++; // 인스턴스 변수 접근
instanceMethod(); // 인스턴스 메서드 접근
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 접근
}
private void instanceMethod() {
System.out.println("instanceValue=" + instanceValue);
}
private static void staticMethod() {
System.out.println("staticValue=" + staticValue);
}
}
이번 예제에서는 접근 제어자를 적극 활용해서 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었습니다.
instanceValue는 인스턴스 변수입니다.
staticValue는 정적 변수(클래스 변수)입니다.
instanceMethod()는 인스턴스 메서드입니다.
staticMethod()는 정적 메서드(클래스 메서드)입니다.
staticCall() 메서드를 봐봅시다.
이 메서드는 정적 메서드입니다.
따라서 static 만 사용할 수 있습니다.
정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생합니다.
코드를 보면 staticCall() -> staticMethod()로 static에서 static을 호출하는 것을 확인할 수 있습니다.
instanceCall() 메서드를 봐봅시다.
이 메서드는 인스턴스 메서드입니다.
모든 곳에서 공용인 static을 호출할 수 있습니다.
따라서 정적 변수, 정적 메서드에 접근할 수 있습니다.
물론 인스턴스 변수, 인스턴스 메서드에도 접근할 수 있습니다.
DecoDataMain
package static2;
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("1. 정적 호출");
DecoData.staticCall();
System.out.println("2. 인스턴스 호출1");
DecoData data1 = new DecoData();
data1.instanceCall();
System.out.println("3. 인스턴스 호출2");
DecoData data2 = new DecoData();
data2.instanceCall();
}
}
실행 결과
1. 정적 호출
staticValue=1
2. 인스턴스 호출1
instanceValue=1
staticValue=2
3. 인스턴스 호출2
instanceValue=1
staticValue=3
정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있습니다.
그래서 인스턴스처럼 참조값의 개념이 없습니다.
특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출합니다.
따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없습니다.
물론 당연한 이야기지만 다음과 같이 객체의 참조값을 직접 매개변수로 전달하면 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있습니다.
public static void staticCall(DecoData data) {
data.instanceValue++;
data.instanceMethod();
}
-
-
-
-
-
-
-
☕️[Java] 자바 메모리 구조
자바 메모리 구조.
자바 메모리 구조 - 비유
자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역, 3개로 나눌 수 있습니다.
메서드 영역: 클래스 정보를 보관합니다. 이 클래스 정보가 붕어빵 틀입니다.
스택 영역: 실제 프로그램이 실행되는 영역입니다. 메서드를 실행할 때 마다 하나씩 쌓입니다.
힙 영역: 객체(인스턴스)가 생성되는 영역입니다. new 명령어를 사용하면 이 영역을 사용합니다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간입니다.
참고로 배열도 이 영역에 생성됩니다.
위 설명한 내용은 쉽게 비유로 한 것이고 실제는 다음과 같습니다.
메서드 영역(Method Are): 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리합니다. 이영역은 프로그램의 모든 영역에서 공유합니다.
클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재합니다.
static영역: static 변수들을 보관합니다.
런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관합니다. 예를 들어서 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리합니다. 이외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리합니다.
스택 영역(Stack Area): 자바 실행 시, 하나의 실행 스택이 생성됩니다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함합니다.
스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임입니다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거됩니다.
힙 영역(Heap Area): 객체(인스턴스)와 배열이 생성되는 영역입니다. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거됩니다.
참고: 스택 영역은 더 정확히는 각 쓰레드별로 하나의 실행 스택이 생성됩니다. 따라서 쓰레드 수 만큼 스택 영역이 생성됩니다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나입니다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있습니다.
메서드 코드는 메서드 영역에
자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생깁니다.
각각의 인스턴스는 내부에 변수와 메서드를 가집니다.
같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유합니다.
따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없습니다.
메서드는 메서드 영역에서 공통으로 관리되고 실행됩니다.
정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행합니다.
-
☕️[Java] 캡슐화
캡슐화
캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나입니다.
캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말합니다.
캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있습니다.
캡슐화는 쉽게 이야기해서 속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 너머지는 모두 내부로 숨기는 것입니다.
이전에 객체 지향 프로그래밍을 설명하면서 캡슐화에 대해 알아보았습니다.
이때는 데이터와 데이터를 처리하는 메서드를 하나로 모으는 것에 초점을 맞추었습니다.
여기서 한발짝 더 나아가 캡슐화를 안전하게 완성할 수 있게 해주는 장치가 바로 “접근 제어자” 입니다.
그럼 어떤 것을 숨기고 어떤 것을 노출해야 할까요?
1. 데이터를 숨겨라.
객체에는 속성(데이터)과 기능(메서드)이 있습니다.
캡슐화에서 가장 필수로 숨겨야 하는 것은 “속성(데이터)” 입니다.
package access;
public class Speaker {
private int volume;
Speaker(int volume) {
this.volume = volume;
}
void volumeUp() {
if (volume >= 100) {
System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
} else {
volume += 10;
System.out.println("음량을 증가합니다.");
}
}
void volumeDown() {
volume -= 10;
System.out.println("volumeDown 호출");
}
void showVolume() {
System.out.println("현재 음량: " + volume);
}
}
위 코드에서의 Speaker의 volume을 봐봅시다.
객체 내부의 데이터를 외부에서 함부로 접근하게 둔다면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있습니다.
결국 모든 안전망을 다 빠져나가게 됩니다.
따라서 캡슐화가 깨집니다.
우리가 자동자를 운전할 때 자동차 부품을 다 열어서 그 안에 있는 속도계를 직접 조절하지 않습니다.
단지 자동차가 제공하는 액셀 기능을 사용해서 액셀을 밟으면 자동차가 나머지는 다 알아서 하는 것입니다.
우리가 일상에서 생각할 수 있는 음악 플레이어를 떠올려봅시다.
음악 플레이어를 사용할 때 그 내부에 들어있는 전원부나, 볼륨 상태의 데이터를 직접 수정할 일이 있을까요?
우리는 그냥 음악 플레이어의 켜고, 끄고, 볼륨을 조절하는 버튼을 누를 뿐입니다.
그 내부에 있는 전원부나, 볼륨의 조절하는 버튼을 누를 뿐입니다.
그 내부에 있는 전원부나, 볼륨의 상태 데이터를 직접 수정하지는 않습니다.
전원 버튼을 눌렀을 때 실제 전원을 받아서 전원을 켜는 것은 음악 플레이어의 일입니다.
볼륨을 높였을 때 내부에 있는 볼륨 장치들을 움직이고 볼륨 수치를 조절하는 것도 음악 플레이어가 스스로 해야하는 일입니다.
쉽게 이야기해서 우리는 음악 플레이어가 제공하는 기능을 통해서 음악 플레이어를 사용하는 것입니다.
복잡하게 음악 플레이어의 내부를 까서 그 내부 데이터까지 우리가 직접 사용하는 것은 아닙니다.
객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 합니다.
2. 기능을 숨겨라.
“객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있습니다.”
이런 기능도 모두 감추는 것이 좋습니다.
우리가 자동차를 운정하기 위해 자동차가 제공하는 복잡한 엔진 조절 기능, 배기 기능까지 우리는 알 필요가 없습니다.
우리는 단지 렉셀과 핸들 정도의 기능만 알면 됩니다.
만약 사용자에게 이런 기능까지 모두 알려준다면, 사용자가 자동차에 대해 너무 많은 것을 알아야 합니다.
“사용자 입장에서 꼭 필요한 기능만 외부에 노출합시다. 나머지 기능은 모두 내부로 숨깁시다.”
“정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화입니다.”
이번에는 잘 캡슐화된 예제를 하나 만들어봅시다.
BankAccount
package access;
public class BankAccount {
private int balance;
public BankAccount() {
balance = 0;
}
// public 메서드: deposit
public void deposit(int amount) {
if (isAmountValid(amount)) {
balance += amount;
System.out.println(amount + "원이 입금되었습니다.");
} else {
System.out.println("유효하지 않은 금액입니다.");
}
}
// public 메서드: withdraw
public void withdraw(int amount) {
if (isAmountValid(amount) && (balance - amount >= 0)) {
balance -= amount;
System.out.println(amount + "원이 출금되었습니다.");
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드: getBalance
public int getBalance() {
return balance;
}
private boolean isAmountValid(int amount) {
// 금액이 0보다 커야함.
return amount > 0;
}
}
BankAccountMain
package access;
public class BankAccountMain {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(10000);
account.withdraw(3000);
System.out.println("balance = " + account.getBalance());
}
}
위 코드는 은행 계좌 기능을 다룹니다.
위 코드는 다음과 같은 기능을 가지고 있습니다.
private
balance : 데이터 필드는 외부에 직접 노출하지 않습니다. BankAccount가 제공하는 메서드를 통해서만 접근할 수 있슷빈다.
isAmountValid() : 입력 금액을 검증하는 기능은 내부에서만 필요한 기능입니다. 따라서 private을 사용했습니다.
pulbic
deposit() : 입금
withdraw() : 출금
getBalance() : 잔고
BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 됩니다.
나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있습니다.
“만약 isAmountValid()를 외부에 노출할 경우 어떻게될까요?”
BankAccount를 사용하는 개발자 입장에서는 사용할 수 있는 메서드가 하나 더 늘었습니다.
여러분이 BankAccount를 사용하는 개발자라면 어떤 생각을 할까요?
아마도 입금과 출금 전에 본인이 먼저 isAmountValid()를 사용해서 검증을 해야 하나? 라고 의문을 가질 것입니다.
“만약 balance 필드를 외부에 노출하면 어떻게 될까요?”
BankAccount를 사용하는 개발자 입장에서는 이 필드를 직접 사용해도 된다고 생각할 수 있습니다.
“왜냐하면 외부에 공개하는 것은 그것을 외부에서 사용해도 된다는 뜻이기 때문입니다.”
“결국 모든 검증과 캡슐화가 깨지고 잔고를 무한정 늘리고 출금하는 심각한 문제가 발생할 수 있습니다.”
“접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도도 낮출 수 있습니다.”
-
-
-
-
-
-
-
-
-
-
☕️[Java] 생성자 - 오버로딩 this()
생성자 - 오버로딩 this()
생성자도 메서드 오버로딩처럼 매개변수만 다르게 해서 여러 생성자를 제공할 수 있습니다.
MemberConstruct - 생성자 추가
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
// 추가
MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
기존 MemberConstruct에 생성자를 하나 추가해서 생성자가 2개가 되었습니다.
MemberConstruct(String name, int age)
MemberConstruct(String name, int age, int grade)
새로 추가한 생성자는 grade를 받지 않습니다. 대신에 grade는 50점이 됩니다.
package construct;
public class ConstructMain2 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16);
MemberConstruct[] members = { member1, member2 };
for (MemberConstruct member : members) {
System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade);
}
}
}
실행 결과
생성자 호출 name=user1,age=15,grade=90
이름:user1 나이:15성적:90
이름:user2 나이:16성적:50
생성자를 오버로딩 한 덕분에 성적 입력이 꼭 필요한 경우에는 grade가 있는 생성자를 호출하면 되고, 그렇지 않은 경우에는 grade가 없는 생성자를 호출하면 됩니다.
grade가 없는 생성자를 호출하면 성적은 50점이 됩니다.
this()
두 생성자를 비교해 보면 코드가 중복되는 부분이 있습니다.
public MemberConstruct(String name, int age) {
this.name = name;
this.age = age;
this.grade = 50;
}
public MemberConstruct(String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
바로 다음 부분입니다.
this.name = name;
this.age = age;
이때 this()라는 기능을 사용하면 생성자 내부에서 자신의 생성자를 호출할 수 있습니다.
참고로 this는 인스턴스 자신의 참조값을 가리킵니다. 그래서 자신의 생성자를 호출한다고 생각하면됩니다.
MemberConstruct - this() 사용
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
// 추가
MemberConstruct(String name, int age) {
this(name, age, 50);
}
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
이 코드는 첫번째 생성자 내부에서 두번째 생성자를 호출합니다.
MemberConstruct(String name, int age) -> MemberConstruct(String name, int age, int grade)
this()를 사용하면 생성자 내부에서 다른 생성자를 호출할 수 있습니다.
이 부분을 잘 활용하면 지금과 같이 중복을 제거할 수 있습니다.
물론 실행 결과는 기존과 같습니다.
this() 규칙
this()는 생성자 코드의 첫줄에만 작성할 수 있습니다.
다음은 규칙 위반입니다.
이 경우 컴파일 오류가 발생합니다.
public MemberConstruct(String name, int age) {
Sytem.out.println("go");
this(name, age, 50);
}
this()가 생성자 코드의 첫줄에 사용되지 않았습니다.
-
-
☕️[Java] this
this
Member - initMember() 추가
package construct;
public class Member {
String name;
int age;
int grade;
// 추가
void initMember( String name, int age, int grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}
MethodInitMain3
package construct;
public class MethodInitMain3 {
public static void main(String[] args) {
Member member1 = new Member();
member1.initMember("user1", 15, 90);
Member member2 = new Member();
member2.initMember("user2", 16, 80);
Member[] members = { member1, member2 };
for (Member member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
}
initMember(...)는 Member에 초기값 설정 기능을 제공하는 메서드입니다.
다음과 같이 메서드를 호출하면 객체의 멤버 변수에 인자로 넘어온 값을 채웁니다.
member1.initMember("user1", 15, 90)
this
Member의 코드를 다시 봐봅시다.
initMember(String name...)의 코드를 보면 메서드의 매개변수에 정의한 String name과 Member의 멤버 변수의 이름이 String name으로 둘다 똑같습니다.
나머지 변수 이름도 name, age, grade로 모두 같습니다.
“멤버 변수와 메스더의 매개변수의 이름이 같으면 둘을 어떨게 구분해야 할까요?”
이 경우 멤버 변수보다 매개변수가 코드 블럭의 안쪽에 있기 때문에 “매개변수가 우선 순위를” 가집니다.
따라서 initMember(String name, ...) 메서드 안에서 name이라고 적으면 매개변수에 접근하게 됩니다.
멤버 변수에 접근하려면 앞에 this.이라고 해주면 됩니다.
여기서 this는 인스턴스 자신의 참조값을 가리킵니다.
진행 과정
this.name = name; // 1. 오른쪽의 name은 매개변수에 접근
this.name = "user"; // 2. name 매개변수의 값 사용
x001.name = "user"; // 3. this.은 인스턴스 자신의 참조값을 뜻함. 따라서 인스턴스의 멤버 변수에 접근.
this 제거
만약 이 예제에서 this를 제거하면 어떻게 될까요?
this.name = name
다음과 같이 수정하면 name은 둘다 매개변수를 뜻하게 됩니다.
따라서 멤버변수의 값이 변경되지 않습니다.
name = name;
정리
매개변수의 이름과 멤버 변수의 이름이 같은 경우 this를 사용해서 둘을 명확하게 구분해야 합니다.
this는 인스턴스 자신을 가리킵니다.
this의 생략
this는 생략할 수 있습니다.
이 경우 변수를 찾을 때 가까운 지역변수(매개변수도 지역변수입니다.)를 먼저 찾고 없으면 그 다음으로 멤버 변수를 찾습니다.
멤버 변수도 없으면 오루가 발생합니다.
다음 예제는 필드 이름과 매개변수의 이름이 서로 다릅니다.
MemberThis
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
nameField = nameParameter;
}
}
“예를 들어서 nameField는 앞에 this가 없어도 멤버 변수에 접근합니다.”
nameField는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다.
이 경우 없으므로 멤버 변수에서 찾습니다.
nameParametr는 먼저 지역변수(매개변수)에서 같은 이름이 있는지 찾습니다.
이 경우 매개변수가 있으므로 매개변수를 사용합니다.
this와 코딩 스타일
“다음과 같이 멤버 변수에 접근하는 경우에 항상 this를 사용하는 코딩 스타일도 있습니다.”
MemberThis
package construct;
public class MemberThis {
String nameField;
void initMember(String nameParameter) {
this.nameField = nameParameter;
}
}
this.nameField를 보면 this를 생략할 수 있지만, 생략하지 않고 사용해도 됩니다.
이렇게 this를 사용하면 이 코드가 멤버 변수를 사용한다는 것을 눈으로 쉽게 확인할 수 있습니다.
그래서 과거에 이런 스타일을 많이 사용하기도 했습니다.
쉽게 이야기해서 this를 강제로 사용해서, 지역 변수(매개변수)와 멤버 변수를 눈에 보이도록 구분하는 것입니다.
“하지만 최근에는 IDE가 발전하면서 IDE가 멤버 변수와 지역 변수를 색상으로 구분해줍니다.”
이런 점 때문에 this는 꼭 필요한 경우에만 사용해도 충분하다 생각한다.
예를 들어 이런 경우 this.name = name -> name이 중복되는 것.
-
☕️[Java] 생성자 - 도입
생성자 - 도입
프로그래밍을 하다보면 객체를 생성하고 이후에 바로 초기값을 할당해야 하는 경우가 많습니다.
따라서 앞서 initMember(...)와 같은 메서드를 매번 만들어야 합니다.
“그래서 대부분의 객체 지향 언어는 객체를 생성하자마자 즉시 필요한 기능을 좀 더 편리하게 수행할 수 있도록 생성자라는 기능을 제공합니다.”
생성자를 사용하면 객체를 생성하는 시점에 즉시 필요한 기능을 수행할 수 있습니다.
생성자는 앞서 살펴본 initMember(...)와 같이 메서드와 유사하지만 몇가지 다른 특징이 있습니다.
아래 코드를 보면서 이해해봅시다.
MemberConstruct
package construct;
public class MemberConstruct {
String name;
int age;
int grade;
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
}
“다음 부분이 바로 생성자입니다.”
MemberConstruct(String name, int age, int grade) {
System.out.println("생성자 호출 name=" + name + ",age=" + age + ",grade=" + grade);
this.name = name;
this.age = age;
this.grade = grade;
}
“생성자는 메서드와 비슷하지만 다음과 같은 차이가 있습니다.”
생성자의 이름은 클래스 이름과 같아야 합니다. 따라서 첫 글자도 대문자로 시작합니다.
생성자는 반환 타입이 없습니다. 비워두워야 합니다.
나머지는 메서드와 같습니다.
ConstructMain1
package construct;
public class ConstructMain1 {
public static void main(String[] args) {
MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
MemberConstruct member2 = new MemberConstruct("user2", 16, 80);
MemberConstruct[] members = { member1, member2 };
for (MemberConstruct member : members) {
System.out.println("이름:" + member.name + " 나이:" + member.age + "성적:" + member.grade);
}
}
}
실행 결과
생성자 호출 name=user1,age=15,grade=90
생성자 호출 name=user2,age=16,grade=80
이름:user1 나이:15성적:90
이름:user2 나이:16성적:80
생성자 호출
생성자는 인스턴스를 생성하고 나서 즉시 호출됩니다.
생성자를 호출하는 방법은 다음 코드와 같이 new 명령어 다음에 생성자 이름과 매개변수에 맞추어 인수를 전달하면 됩니다.
new 생성자이름(생성자에 맞는 인수 목록)
new 클래스이름(생성자에 맞는 인수 목록)
참고로 생성자 이름이 클래스 이름이기 때문에 둘다 맞는 표현입니다.
new MemberConstruct("user1", 15, 90)
이렇게 하면 인스턴스를 생성하고 즉시 해당 생성자를 호출합니다.
여기서는 Memeber 인스턴스를 생성하고 바로 MemberConstruct(String name, int age, int grade) 생성자를 호출합니다.
참고로 new 키워드를 사용해서 객체를 생성할 때 마지막에 괄호()도 포함해야 하는 이유가 바로 생성자 때문입니다. 객체를 생성하면서 동시에 생성자를 호출한다는 의미를 포함합니다.
생성자 장점
중복 호출 제거
생성자가 없던 시절에는 생성 직후에 어떤 작업을 수행하기 위해 다음과 같이 메서드를 직접 한번 더 호출해야 했습니다.
“생성자 덕분에 객체를 생성하면서 동시에 생성 직후에 필요한 작업을 한번에 처리할 수 있게 되었습니다.”
// 생성자 등장 전
MemberInit member = new MemberInit();
member.initMember("user1", 15, 90);
// 생성자 등장 후
MemberConstruct member = new MemberConstruct("user1", 15, 90);
제약 - 생성자 호출 필수
위 코드에서 생성자 등장 전 코드를 보며 이해해봅시다.
“이 경우 initMember(...)를 실수로 호출하지 않으면 어떻게 될까요?”
이 메서드를 실수로 호출하지 않아도 프로그램은 작동합니다.
하지만 학생의 이름과 나이, 성적 데이터가 없는 상태로 프로그램이 작동하게 됩니다.
“만약에 이 값들을 필수로 반드시 입력해야 한다면, 시스템에 큰 문제가 발생할 수 있습니다.”
결국 아무 정보가 없는 유령 학생이 시스템 내부에 등장하게 됩니다.
생성자의 진짜 장점은 객체를 생성할 때 직접 정의한 생성자가 있다면 “직접 정의한 생성자를 반드시 호출” 해야 한다는 점입니다.
참고로 생성자를 메서드 오버로딩 처럼 여러개 정의할 수 있는데, 이 경우에는 하나만 호출하면 됩니다.
“MemberConstruct 클래스의 경우 다음 생성자를 직접 정의했기 때문에 직접 정의한 생성자를 반드시 호출해야 합니다.”
MemberConstruct(String name, int age, int grade) { ... }
다음과 같이 직접 정의한 생성자를 호출하지 않으면 컴파일 오류가 발생합니다.
MemberConstruct member3 = new MemberConstruct(); // 컴파일 오류 발생
member3.name = "user1";
컴파일 오류 메시지
java: constructor MemberConstruct in class construct.MemberConstruct cannot be applied to given types;
required: java.lang.String,int,int
found: no arguments
reason: actual and formal argument lists differ in length
컴파일 오류는 IDE에서 즉시 확인할 수 있는 좋은 오류입니다.
이 경우 개발자는 객체를 생성할 때, 직접 정의한 생성자를 필수로 호출해야 한다는 것을 바로 알 수 있습니다.
그래서 필요한 생성자를 찾아서 다음과 같이 호출할 것입니다.
MemberConstruct member = new MemberConstruct("user1", 15, 90);
“생성자 덕분에 학생의 이름, 나이, 성적은 항상 필수로 입력하게 됩니다.”
따라서 아무 정보가 없는 유령 회원이 시스템 내부에 등장할 가능성을 원천 차단합니다.
“생성자를 사용하면 필수값 입력을 보장할 수 있습니다.”
참고: 좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램입니다.
-
☕️[Java] 생성자 - 필요한 이유
생성자 - 필요한 이유.
객체를 생성하는 시점에 어떤 작업을 하고 싶다면 “생성자(Construct)” 을 이용하면 됩니다.
생성자를 알아보기 전에 먼저 생성자가 왜 필요한지 코드로 간단히 알아봅시다.
MemberInit
package construct;
public class MemberInit {
String name;
int age;
int grade;
}
MethodInitMain1
package construct;
public class MethodInitMain1 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
member1.name = "user1";
member1.age = 15;
member1.grade = 90;
MemberInit member2 = new MemberInit();
member2.name = "user2";
member2.age = 16;
member2.grade = 80;
MemberInit[] members = { member1, member2 };
for (MemberInit member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
}
실행 결과
아름:user1 나이:15 성적:90
아름:user2 나이:16 성적:80
회원 객체를 생성하고 나면 name, age, grade 같은 변수에 초기값을 설정합니다.
아마도 회원 객체를 제대로 사용하기 위해서는 객체를 생성하자 마자 이런 초기값을 설정해야 할 것입니다.
이 코드에는 회원의 초기값을 설정하는 부분이 계속 반복됩니다.
메서드를 사용해서 반복을 제거해봅시다.
MethodInitMain2
package construct;
public class MethodInitMain2 {
public static void main(String[] args) {
MemberInit member1 = new MemberInit();
initMember(member1, "user1", 15, 90);
MemberInit member2 = new MemberInit();
initMember(member2, "user2", 16, 80);
MemberInit[] members = { member1, member2 };
for (MemberInit member : members) {
System.out.println("아름:" + member.name + " 나이:" + member.age + " 성적:" + member.grade);
}
}
static void initMember(MemberInit member, String name, int age, int grade) {
member.name = name;
member.age = age;
member.grade = grade;
}
}
initMember(...) 메서드를 사용해서 반복을 제거했습니다.
그런데 이 메서드는 대부분 MemberInit 객체의 멤버 변수를 사용합니다.
우리는 앞서 객체 지향에 대해서 학습했습니다.
이런 경우 속성과 기능을 한 곳에 두는 것이 더 나은 방법입니다.
쉽게 이야기해서 MemberInit이 자기 자신의 데이터를 변경하는 기능(메서드)을 제공하는 것이 좋습니다.
-
-
-
-
-
☕️[Java] 객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍 vs 절차 지향 프로그래밍
객체 지향 프로그래밍과 절차 지향 프로그래밍은 서로 대치되는 개념이 아닙니다.
객체 지향이라도 프로그램의 작동 순서는 중요합니다.
다만 어디에 더 초점을 맞추는가에 둘의 차이가 있습니다.
객체 지향의 경우 객체의 설계와 관계를 중시합니다.
반면 절차 지향의 경우 데이터와 기능이 분리되어 있고, 프로그램이 어떻게 작동하는지 그 순서에 초점을 맞춥니다.
절차 지향 프로그래밍
절차 지향 프로그래밍은 이름 그대로 절차를 지향합니다.
쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식입니다.
절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식입니다.
즉, “어떻게”를 중심으로 프로그래밍 합니다.
객체 지향 프로그래밍
객체 지향 프로그래밍은 이름 그대로 객체를 지향합니다.
쉽게 이야기해서 객체를 중요하게 생각하는 방식입니다.
객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객제들 간의 상호작용을 중심으로 프로그래밍하는 방식입니다.
즉 “무엇을” 중심으로 프로그래밍 합니다.
둘의 중요한 차이
절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있습니다.
반면 객체 지향에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 ‘객체’ 안에 함께 포함되어 있습니다.
객체란?
세상의 모든 사물을 단순하게 추상화해보면 속성(데이터)과 기능(메서드) 딱 2가지로 설명할 수 있습니다.
자동차
속성: 색상, 속도
기능: 엑셀, 브레이크, 문 열기, 문 닫기
동물
속성: 색상, 키, 온도
기능: 먹는다, 걷는다
게임 캐릭터
속성: 레벨, 경험치, 소유한 아이템들
기능: 이동, 공격, 아이템 획득
“객체 지향 프로그래밍은 모든 사물을 속성과 기능을 가진 객체로 생각하는 것 입니다.”
객체에는 속성과 기능만 존재합니다.
이렇게 단순화하면 세상에 있는 객체들을 컴퓨터 프로그램으로 쉽게 설계할 수 있습니다.
이런 장점들 덕분에 지금은 객체 지향 프로그래밍이 가장 많이 사용됩니다.
참고로 실세계와 객체가 항상 1:1로 매칭되는 것은 아닙니다.
객체 지향의 특징은 속성과 기능을 하나로 묶는 것 뿐만 아니라 캡슐화, 상속, 다형성, 추상화, 메시지 전달 같은 다양한 특징들이 있습니다.
-
-
-
☕️[Java] 변수와 초기화
변수와 초기화
변수의 종류
멤버 변수(필드): 클래스에 선언.
지역 변수: 메서드에 선언, 매개변수도 지역 변수의 한 종류입니다.
멤버 변수, 필드 예시
public class Student {
String name;
int age;
int grade;
}
name, age, grade 는 멤버 변수입니다.
지역 변수 예시
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
Student student2 = new Student();
}
}
student1, student2는 지역 변수입니다.
public class MethodChange1 {
public static void main(String[] args) {
int a = 10;
System.out.println("메서드 호출 전: a = " + a);
changePrimitive(a);
System.out.println("메서드 호출 후: a = " + a);
}
static void changePrimitive(int x) {
x = 20;
}
}
a, x(매개변수, Parameter)는 지역변수입니다.
지역 변수는 이름 그대로 “특정 지역에서만 사용되는 변수라는 뜻입니다.”
예를 들어서 변수 x는 changePrimitive() 메서드의 블록에서만 사용됩니다.
changePrimitive() 메서드가 끝나면 제거됩니다.
a 변수도 마찬가지입니다. main() 메서드가 끝나면 제거됩니다.
변수의 값 초기화
멤버 변수: 자동 초기화.
인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화됩니다.
숫자(int) = 0, boolean = false, 참조형 = null(null 값은 참조할 대상이 없다는 뜻으로 사용됩니다.)
개발자가 초기값을 직접 지정할 수 있습니다.
지역 변수: 수동 초기화.
지역 변수는 항상 직접 초기화해야 합니다.
“멤버 변수의 초기화를 살펴봅시다”
InitData
package ref;
public class InitData {
int value1; // 초기화 하지 않음
int value2 = 10; // 10으로 초기화
}
value1은 초기값을 지정하지 않았고, value2는 초기값을 10으로 지정했습니다.
InitMain
package ref;
public class initMain {
public static void main(String[] args) {
InitData data = new InitData();
System.out.println("value1 = " + data.value1);
System.out.println("value2 = " + data.value2);
}
}
실행 결과
value1 = 0
value2 = 10
value1은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화 됩니다.
숫자는 0으로 초기화됩니다.
value2는 10으로 초기값을 지정해두었기 때문에 객체를 생성할 때 10으로 초기화됩니다.
-
☕️[Java] 참조형과 메서드 호출 - 활용
참조형과 메서드 호출 - 활용
아래의 코드 class1.ClassStart3 코드에는 중복되는 부분이 2가지가 있습니다.
name, age, grade에 값을 할당하는 부분.
학생 정보를 출력하는 부분.
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
“이러한 중복은 메서드를 통해 손쉽게 제거할 수 있습니다.”
메서드에 객체 전달
다음과 같이 코드를 작성해봅시다.
Student
package ref;
public class Student {
String name;
int age;
int grade;
}
ref 패키지에도 Student 클래스를 만듭니다.
Method1
package ref;
public class Method1 {
public static void main(String[] args) {
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static void initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
참조형은 메서드를 호출할 때 참조값을 전달합니다.
따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있습니다.
initStudent(Student student, ...) : 전달한 학생 객체의 필드에 값을 설정합니다.
printStudent(Student student, ...) : 전달한 학생 객체의 필드 값을 읽어서 출력합니다.
initStudent() 메서드 호출 분석
initStudent(Student student, String name, int age, int grade) {
student.name = name;
student.age = age;
student.grade = grade;
}
student1이 참조하는 Student 인스턴스에 값을 편리하게 할당하고 싶어서 initStudent() 메서드를 만들었습니다.
이 메서드를 호출하면서 student1을 전달합니다.
그러면 student1의 참조값(30f39991)이 매개변수 student에 전달됩니다.
이 참조값을 통해 initStudent() 메서드 안에서 student1이 참조하는 것과 동일한 30f39991 Student 인스턴스에 접근하고 값을 변경할 수 있습니다.
참고: 30f39991은 실제 student1의 참조값입니다.
System.out.println("student1 참조값 : " + student1); 의 결과가 student1 참조값 : ref.Student@30f39991 이였습니다.
주의!
package ref;
import class1 Student;
public class Method1 {
...
}
import class1.Student;이 선언되어 있으면 안됩니다.
이렇게 되면 class1 패키지에서 선언한 Student를 사용하게 됩니다.
이 경우에는 접근 제어자 때문에 컴파일 오류가 발생합니다.
만약 선언되어 있다면 삭제해야 합니다. 삭제하면 같은 패키지에 있는 ref.Student를 사용합니다.
메서드에서 객체 반환
조금 더 코드를 리팩토링 시켜봅시다. 다음 코드에도 중복이 있습니다.
Student student1 = new Student();
initStudent(student1, "학생1", 15, 90);
Student student2 = new Student();
initStudent(student2, "학생2", 16, 80);
바로 객체를 생성하고, 초기값을 설정하는 부분입니다.
이렇게 2번 반복되는 부분을 하나로 합져봅시다.
“다음과 같이 기존 코드를 변경해봅시다.”
Method2
package ref;
public class Method2 {
public static void main(String[] args) {
Student student1 = createStudent("학생1", 15, 90);
Student student2 = createStudent("학생2", 16, 80);
printStudent(student1);
printStudent(student2);
}
static Student createStudent(String name, int age, int grade) {
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
static void printStudent(Student student) {
System.out.println("이름:" + student.name + " 나이:" + student.age + " 성적:" + student.grade);
}
}
createStudent() 라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드안에 함께 포함했습니다.
“이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리합니다.”
그런데 메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 합니다.
그래야 메서드 밖에서 이 객체를 사용할 수 있습니다.
메서드는 호출 결과를 반환(return)을 할 수 있습니다.
메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 됩니다.
createStudent() 메서드 호출 분석
createStudent(String name, int age, int grade) {
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
메서드 내부에서 인스턴스를 생성한 후에 참조값을 메서드 외부로 반환했습니다.
이 참조값만 있으면 해당 인스턴스에 접근할 수 있습니다.
여기서는 student1에 참조값을 보관하고 사용합니다.
진행 과정
Student student1 = createStudent("학생1", 15, 90); // 메서드 호출후 결과 반환
Student student1 = student(30f39991); // 참조형인 student를 반환
Student student1 = 30f39991; // student의 참조값 대입
student1 = 30f39991;
createStudent() 는 생성한 Student 인스턴스의 참조값을 반환합니다.
이렇게 반환된 참조값을 student1 변수에 저장합니다.
앞으로는 student1을 통해 Student 인스턴스를 사용할 수 있습니다.
-
☕️[Java] 기본형과 참조형(3) - 메서드 호출
기본형과 참조형(3) - 메서드 호출
대원칙: 자바는 항상 변수의 값을 복사해서 대입합니다.
자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것입니다.
기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입합니다.
기본형이면 변수에 들어 있는 실제 사용하는 값을 복사해서 대입합니다.
참조형이면 변수에 들어 있는 참조값을 복사해서 대입합니다.
메서드 호출도 마찬가지입니다. 메서드를 호출할 때 사용하는 매개변수(parameter)도 결국 변수일 뿐 입니다.
따라서 메서드를 호출할 때 매개변수(parameter)에 값을 전달하는 것도 앞서 설명한 내용과 같이 값을 복사해서 전달합니다.
다음 예시 코드를 봐봅시다.
기본형과 메서드 호출
package ref;
public class MethodChange1 {
public static void main(String[] args) {
int a = 10;
System.out.println("메서드 호출 전: a = " + a);
changePrimitive(a);
System.out.println("메서드 호출 후: a = " + a);
}
static void changePrimitive(int x) {
x = 20;
}
}
실행 결과
메서드 호출 전: a = 10
메서드 호출 후: a = 10
1. 메서드 호출
메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달합니다.
int x = a
이 코드는 다음과 같이 해석할 수 있습니다.
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다 따라서 변수 a,x 각각 숫자 10을 가지고 있습니다.
2. 메서드 안에서 값을 변경
int a = 10;
changePrimitive(a);
메서드 안에서 x = 20으로 새로운 값을 대입합니다.
결과적으로 x의 값만 20으로 변경되고, a의 값은 10으로 유지됩니다.
3. 메서드 종료
int a = 10;
changePrimitive(a);
메서드 종료후 값을 확인해보면 a는 10이 출력되는 것을 확인할 수 있습니다.
참고로 메서드가 종료되면 매개변수 x는 제거됩니다.
참조형과 메서드 호출
package ref;
public class MethodChange2 {
public static void main(String[] args) {
Data dataA = new Data();
dataA.value = 10;
System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
changeReference(dataA);
System.out.println("메서드 호출 전: dataA.value = " + dataA.value);
}
static void changeReference(Data dataX) {
dataX.value = 20;
}
}
실행 결과
메서드 호출 전: dataA.value = 10
메서드 호출 전: dataA.value = 20
1. 메서드 호출
메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 전달합니다.
이 코드는 다음과 같이 해석할 수 있습니다.
int dataX = dataA;
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입합니다.
변수 dataA는 참조값 x001을 가지고 있다는 가정하에 dataA는 참조값을 복사해서 전달합니다.
따라서 변수 dataA, dataX 둘 다 같은 참조값인 x001을 가지게 됩니다.
이제 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있습니다.
2. 메서드 안에서 값을 변경
static void changeReference(Data dataX) {
dataX.value = 20;
}
메서드 안에서 dataX.value = 20으로 새로운 값을 대입합니다.
참조값을 통해 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경했습니다.
dataA, dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataX.value는 둘다 20이라는 값을 가집니다.
3. 메서드 종료
메서드 종료후 dataA.value의 값을 확인해보면 다음과 같이 20으로 변경된 것을 확인할 수 있습니다.
메서드 호출 전: dataA.value = 10
메서드 호출 전: dataA.value = 20
기본형과 참조형의 메서드 호출
자바에서 메서드의 매개변서(Parameter)는 항상 값에 의해 전달됩니다.
그러나 이 값이 실제 값이냐, 참조(메모리 주소)값이냐에 따라 동작이 달라집니다.
기본형: 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)의 값을 변경해도, 호출자의 변수 값에는 영향이 없습니다.
참조형: 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달됩니다. 이 경우, 메서드 내부에서 매개변수(Parameter)로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경됩니다.
-
-
-
☕️[Java] 기본형과 참조형(1)
기본형과 참조형(1)
자바에서 참조형을 제대호 이해하는 것은 정말 중요합니다.
변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있습니다.
사용하는 값을 변수에 직접 넣을 수 있는 기본형
이전 포스팅의 예시 코드에서 본 Student student1 과 같이 객체가 저장된 메모리의 위치를 가르키는 참조값을 넣을 수 있는 참조형으로 분류할 수 있습니다.
기본형(Primitive Type)
Int, long, double, boolean 처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입을 “기본형” 이라고 합니다.
참조형(Reference Type)
Student student1, int[] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입을 “참조형” 이라고 합니다.
참조형은 객체 또는 배열에 사용됩니다.
쉽게 이야기해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만 참조형 변수에는 위치(참조값)이 들어가 있습니다.
참조형 변수를 통해서 뭔가 하려면 결국 참조값을 통해 위치로 이동해야 합니다.
기본형 vs 참조형 - 기본
“기본형” 은 숫자 10, 20과 같이 실제 사용하는 값을 변수에 담을 수 있습니다.
때문에 해당 값을 바로 사용할 수 있습니다.
“참조형” 은 실제 사용하는 값을 변수에 담는 것이 아닙니다.
이름 그대로 실제 객체의 위치(참조, 주소)를 저장합니다.
참조형에는 객체와 배열이 있습니다.
“객체” 는 .(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있습니다.
“배열” 은 []를 통해서 메모리 상에 생성된 배열을 찾아야가 사용할 수 있습니다.
기본형 vs 참조형 - 계산
기본형은 들어있는 값을 그대로 계산에 사용할 수 있습니다.
예) 더하고 빼고, 사용하고 등등,(숫자 같은 것들은 바로 계산할 수 있습니다.)
참조형은 들어있는 참조값을 그대로 사용할 수 없습니다.
주소지만 가지고는 할 수 있는게 없습니다.
주소지에 가야 실체가 있기 때문입니다.
예) 더하고 빼고 사용하고를 못합니다. 참조값만 가지고는 계산할 수 있는 것이 없습니다.
“기본형은 연산이 가능하지만 참조형은 연산이 불가능합니다.”
int a = 10, b = 20;
int sum = a + b;
기본형은 변수에 실제 사용하는 값이 담겨있습니다.
따라서 +, -와 같은 연산이 가능합니다.
Student s1 = new Student();
Student s2 = new Student();
s1 + s2 // 오류 발생.
참초형은 변수에 객체의 위치인 참조값이 들어있습니다.
참조값은 계산에 사용할 수 없습니다. 따라서 오류가 발생합니다.
“물론 다음과 같이 .(dot)을 통해 객체의 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있습니다.”
Student s1 = new Student();
s1.grade = 100;
Student s2 = new Student();
s2 grade 90;
int sum = s1.grade + s2.grade // 연산 가능
쉽게 이해하는 팁
“기본형을 제외한 나머지는 모두 참조형입니다.”
기본형은 소문자로 시작합니다.
int, long, double, boolean 모두 소문자로 시작합니다.
기본형은 자바가 기본으로 제공하는 데이터 타입입니다.
이러한 기본형은 개발자가 새로 정의할 수 없습니다.
개발자는 참조형인 클래스만 직접 정의할 수 있습니다.
클래스는 대문자로 시작합니다.
Student, String, 등…
클래스는 모두 참조형입니다.
“참고 - String”
자바에서 String은 특별합니다.
String은 사실 클래스입니다. 즉, 참조형입니다.
그런데 기본형처럼 문자 값을 바로 대입할 수 있습니다.
문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공합니다.
String에 대한 자세한 내용은 추후에 설명하겠습니다.
-
-
☕️[Java] 배열 도입
배열 도입
“배열” 을 사용하면 특정 타입을 연속한 데이터 구조로 묶어서 편리하게 관리할 수 있습니다.
Student 클래스를 사용한 변수들도 Student 타입이기 때문에 학생도 “배열” 을 사용해서 “하나의 데이터 구조로 묶어서 관리” 할 수 있습니다.
클래스의 도입 포스팅 예시 코드 참고 -> 학생 클래스.
Student 타입을 사용하는 “배열” 을 도입해봅시다.
package class1;
public class ClassStart4 {
public static void main(String[] args) {
Student student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
Student[] students = new Student[2];
students[0] = student1;
students[1] = student2;
for (int i = 0; i < students.length; i++) {
System.out.println("이름:" + students[i].name + " 나이:" + students[i].age + " 성적:" + students[i].grade);
}
}
}
코드를 분석해 봅시다.
Student student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
Student 클래스를 기반으로 student1, student2 인스턴스를 생성합니다. 그리고 필요한 값을 채워둡니다.
배열에 참조값 대입
이번에는 Student를 담을 수 있는 배열을 생성하고, 해당 배열에 student1, student2 인스턴스를 보관해봅시다.
Student[] students = new Student[2];
Student 변수를 2개 보관할 수 있는 사이즈 2의 배열을 만듭니다.
Student 타입의 변수는 Student 인스턴스의 “참조값을 보관” 합니다.
Student 배열의 각각의 항목도 “Student 타입의 변수일 뿐” 입니다. 따라서 “Student 타입의 참조값을 보관” 합니다.
student1, student2 변수를 생각해보면 Student 타입의 참조값을 보관합니다.
배열에는 아직 참조값을 대입하지 않았기 때문에 참조값이 없다는 의미의 null 값으로 초기화 됩니다.
이제 배열에 객체를 보관해보겠습니다.
students[0] = student1;
students[1] = student2;
// 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다.
students[0] = x001;
students[1] = x002;
잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.”
student1, student2에는 참조값이 보관되어 있습니다.
따라서 이 참조값이 배열에 저정됩니다.
또는 student1, student2에 보관된 참조값을 읽고 복사해서 배열에 대입한다고 표현합니다.
이제 배열은 x001, x002의 참조값을 가집니다.
참조값을 가지고 있기 때문에 x001(학생1), x002(학생), Student 인스턴스에 모두 접근할 수 있습니다.
너무 중요해서 한 번더 강조합니다 잊지 말자 자바의 대원칙: “자바에서 대입은 항상 변수에 들어 있는 값을 복사한다.”
students[0] = student1;
students[1] = student2;
// 자바에서 대입은 항상 변수에 들어 있는 값을 복사합니다.
students[0] = x001;
students[1] = x002;
자바에서 변수의 대입(=)은 모두 변수에 들어있는 값을 복사해서 전달하는 것입니다.
이 경우 오른쪽 변수인 student1, student2에는 참조값이 들어있습니다.
그래서 이 값을 복사해서 왼쪽에 있는 배열에 전달합니다.
따라서 기존 student1, student2에 들어있던 참조값은 당연히 그대로 유지됩니다.
주의!!
변수에는 인스턴스 자체가 들어있는 것이 아닙니다!
“인스턴스의 위치를 가리키는 참조값이 들어있을 뿐입니다!!”
따라서 대입(=)시에 인스턴스가 복사되는 것이 아니라 참조값만 복사됩니다.
배열에 들어있는 객체 사용
배열에 들어있는 객체를 사용하려면 먼저 배열에 접근하고, 그 다음에 객체에 접근하면 됩니다.
이전에 설명한 그림과 코드를 함께 보면 쉽게 이해가 될 것입니다.
학생1 예제
System.out.println(students[0].name); // 배열 접근 시작
System.out.println(x005[0].name); // [0]를 사용해서 x005 배열의 0번 요소에 접근
System.out.println(x001.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생1");
학생2 예제
System.out.println(students[1].name); // 배열 접근 시작
System.out.println(x005[1].name); // [0]를 사용해서 x005 배열의 1번 요소에 접근
System.out.println(x002.name); // .(dot)을 사용해서 참조값으로 객체에 접근
System.out.println("학생2");
-
-
☕️[Java] 클래스, 객체, 인스턴스 정리
클래스, 객체, 인스턴스 정리.
클래스 - Class
클래스는 객체를 생성하기 위한 “틀” 또는 “설계도” 입니다.
클래스는 객체가 가져야 할 “속성(변수)” 와 “기능(메서드)” 를 정의합니다.
예를 들어 학생이라는 클래스는 속성(변수)으로 name, age, grade를 가집니다.
참고로 기능(메서드)은 추후에 설명합니다. 지금은 속성(변수)에 집중합시다.
“틀” : 붕어빵 틀을 생각해봅시다.
붕어빵 틀은 붕어빵이 아닙니다! 이렇게 생긴 붕어빵이 나왔으면 좋겠다고 만드는 틀일 뿐입니다.
실제 먹을 수 있는 것이 아닙니다. 실제 먹을 수 있는 팥 붕어빵을 객체 또는 인스턴스라고 합니다.
“설계도” : 자동차 설계도를 생각해봅시다.
자동차 설계도는 자동차가 아닙니다! 설계도는 실제 존재하는 것이 아니라 개념으로만 있는 것입니다.
설계도를 통한 생산한 실제 존재하는 흰색 테슬라 모델 Y 자동차를 객체 또는 인스턴스라고 합니다.
객체 - Object
객체는 클래스에서 정의한 속성과 기능을 가진 실체입니다.
객체는 서로 독립적인 상태를 가집니다.
예를 들어 위 코드에서 student1은 학생1의 속성을 가지는 객체이고, student2는 학생2의 속성을 가지는 객체입니다.
student1 과 student2는 같은 클래스에서 만들어졌지만, 서로 다른 객체입니다.
인스턴스 - Instance
인스턴스는 특정 클래스로부터 생성된 객체를 의미합니다.
그래서 객체와 인스턴스라는 용어는 자주 혼용됩니다.
인스턴스는 주로 객체가 어떤 클래스에 속해 있는지 강조할 때 사용합니다.
예를 들어서 “student1 객체는 Student 클래스의 인스턴스다.” 라고 표현합니다.
객체 vs 인스턴스
둘 다 클래스에서 나온 실체라는 의미에서 비슷하게 사용되지만, 용어상 인스턴스는 “객체보다 좀 더 관계에 초점을 맞춘 단어입니다.”
보통 “student1은 Student의 객체이다.” 라고 말하는 대신 “student1은 Student의 인스턴스이다.” 라고 특정 클래스와의 “관계를 명확히 할 때 인스턴스” 라는 용어를 주로 사용합니다.
좀 더 쉽게 풀어보자면, 모든 인스턴스는 객체이지만, 우리가 “인스턴스” 라고 부르는 순간 “특정 클래스로부터 그 객체가 생성되었음을 강조하고 싶을 때입니다.”
예를 들어, student1은 객체이지만, 이 객체가 Student 클래스로부터 생성된다는 점을 명확히 하기 위해 student1을 Student의 인스턴스라고 부릅니다.
하지만 둘 다 클래스에서 나온 실체라는 핵심 의미는 같기 때문에 보통 둘을 구분하지 않고 사용합니다.
-
☕️[Java] 클래스 도입
클래스 도입
클래스를 사용해서 “학생”이라는 개념을 만들고, 각 학생 별로 본인의 이름, 나이, 성적을 관리해봅시다.
우선 코드를 봐봅시다.
package class1;
public class Student {
String name;
int age;
int grade;
}
class 키워드를 사용해서 학생 클래스(Student)를 정의합니다.
학생 클래스는 내부에 이름(name), 나이(age), 성적(grade) 변수를 가집니다.
이렇게 “클래스에 정의한 변수들을 멤버 변수, 또는 필드” 라 합니다.
멤버 변수(Member Variable) : 이 변수들은 특정 클래스에 소속된 멤버이기 때문에 이렇게 부릅니다.
필드(Field) : 데이터 항목을 가르키는 전통적인 용어입니다. 데이터베이스, 엑셀 등에서 데이터 각각의 항목을 필드라고 합니다.
자바에서 멤버 변수, 필드는 같은 뜻입니다.
클래스에 소속된 변수를 뜻합니다.
클래스는 관례상 대문자로 시작하고 낙타 표기법을 사용합니다.
예) Student, User, MemberService
package class1;
public class ClassStart3 {
public static void main(String[] args) {
Student student1;
student1 = new Student();
student1.name = "학생1";
student1.age = 15;
student1.grade = 90;
Student student2 = new Student();
student2.name = "학생2";
student2.age = 16;
student2.grade = 80;
System.out.println("이름:" + student1.name + " 나이:" + student1.age + " 성적:" + student1.grade);
System.out.println("이름:" + student2.name + " 나이:" + student2.age + " 성적:" + student2.grade);
}
}
실행 결과
이름:학생1 나이:15 성적:90
이름:학생2 나이:16 성적:80
클래스와 사용자 정의 타입
“타입” 은 데이터의 종류나 형태를 나타냅니다.
int라고 하면 정수 타입, String이라고 하면 문자 타입입니다.
그러면 학생(Student)이라는 타입을 만들면 되지 않을까요?
클래스를 사용하면 int, String과 같은 타입을 직접 만들 수 있습니다.
사용자가 직접 정의하는 “사용자 정의 타입” 을 만들려면 설계도가 필요합니다.
이 “이 설계도가 바로 클래스” 입니다.
“설계도”인 “클래스”를 사용해서 “실제 메모리에 만들어진 실체를 객체 또는 인스턴스” 라 합니다.
“클래스”를 통해서 사용자가 원하는 종류의 데이터 타입을 마음껏 정의할 수 있습니다.
용어: 클래스, 객체, 인스턴스
클래스는 설계도이고, 이 설계도를 기반으로 실제 메모리에 만들어진 실체를 “객체 또는 인스턴스” 라 합니다.
둘다 같은 의미로 사용됩니다.
여기서는 학생(Student) 클래스를 기반으로 학생1(student1), 학생2(student2) 객체 또는 인스턴스를 만들었습니다.
코드 분석
1. 변수 선언.
Student student1 // Student 변수 선언
Student student1
Student “타입” 을 받을 수 있는 변수를 선언합니다.
int는 정수를, String은 문자를 담을 수 있듯이 “Student는 Student 타입의 객체(인스턴스)를 받을 수 있습니다.”
2. 객체 생셩.
student1 = new Student() // Student 인스턴스 생성
student1 = new Student() 코드를 나누어 분석해봅시다.
객체를 사용하려면 먼저 설계도인 클래스를 기반으로 객체(인스턴스)를 생성해야 합니다.
new Student()에서의 new는 새로 생성한다는 뜻 입니다.
new Student()는 Student 클래스 정보를 기반으로 새로운 객체를 생성하라는 뜻입니다.
이렇게 하면 메모리에 실제 Student 객체(인스턴스)를 생성합니다.
객체를 생성할 때는 new 클래스명()을 사용하면 됩니다. 마지막에 ()도 추가해야합니다.
Student 클래스는 String name, int age, int grade 멤버 변수를 가지고 있습니다.
이 변수를 사용하는데 필요한 메모리 공간도 함께 확보합니다.
3. 참조값 보관
student1 = x001l // Student 인스턴스 참조값 보관
객체를 생성시 자바는 메모리 어딘가에 있는 이 객체에 접근할 수 있는 참조값(주소)(x001)을 반환합니다.
여기셔 x001이라고 표현한 것이 참조값입니다.(실제로 x001처럼 표현되는 것은 아니고 이해를 돕기 위한 예시입니다.)
new 키워드를 통해 객체가 생성되고 나면 참조값을 반환합니다.
앞서 선언한 변수인 Student student1에 생성된 객체의 참조값(x001)을 보관합니다.
Student student1 변수는 이제 메모리에 존재하는 실제 Student 객체(인스턴스)의 참조값을 가지고 있습니다.
student1 변수는 방금 만든 객체에 접근할 수 있는 참조값을 가지고 있습니다.
따라서 이 변수를 통해서 객체를 접근(참조)할 수 있습니다. 쉽게 이야기해서 student1 변수를 통해 메모리에 있는 실제 객체를 접근하고 사용할 수 있습니다.
참조값을 변수에 보관해야 하는 이유
객체를 생성하는 new Student() 코드 자체에는 아무런 이름이 없습니다.
“이 코드는 단순히 Student 클래스를 기반으로 메모리에 실제 객체를 만드는 것 입니다.”
따라서 생성한 객체에 접근할 수 있는 방법이 필요합니다.
이런 이유로 객체를 생성할 때 반환되는 참조값을 어딘가에 보관해두어야 합니다.
앞서 Student student1 변수에 참조값(x001)을 저장해두었으므로 저장한 참조값(x001)을 통해서 실제 메모리에 존재하는 객체에 접근할 수 있습니다.
지금까지 설명한 내용을 간단히 풀어보면 다음과 같습니다.
Student student1 = new Student(); // 1. Student 객체 생성
Student student1 = x001; // 2. new Student()의 결과로 x001 참조값 변환
student1 = x001; // 3. 최종 결과
이후에 학생2(student2)까지 생성하면 다음과 같이 Student 객체(인스턴스)가 메모리에 2개 생성됩니다.
“각각의 참조값이 다르므로 서로 구분할 수 있습니다.”
참조값을 확인하고 싶다면 다음과 같이 객체를 담고 있는 변수를 출력해보면 됩니다.
System.out.println(student1);
System.out.println(student2);
출력 결과
class1.Student@30f39991
class1.Student@452b3a41
@ 앞은 패키지 + 클래스 정보를 뜻합니다. @ 뒤에 16진수는 참조값을 뜻합니다.
-
☕️[Java] 클래스가 필요한 이유.
클래스가 필요한 이유.
먼저 아래의 학생 정보 출력 프로그램을 만들어 봅시다.
이 프로그램은 두 명의 학생 정보를 출력하는 프로그램입니다.
각 학생은 이름, 나이, 성적을 가지고 있습니다.
요구사항
첫 번째 학생의 이름은 “학생1”, 나이는 15, 성적은 90 입니다.
두 번째 학생의 이름은 “학생2”, 나이는 16, 성적은 80 입니다.
각 학생의 정보를 다음과 같은 형식으로 출력해야 합니다: "이름: [이름] 나이: [나이] 성적: [성적]"
변수를 사용해서 학생 정보를 저장하고 변수를 사용해서 학생 정보를 출력해야 합니다.
예시 출력
이름: 학생1 나이: 15 성적: 90
이름: 학생2 나이: 16 성적: 80
변수를 사용해서 이 문제를 풀어보면 다음과 같이 프로그램을 만들어볼 수 있습니다.
package class1;
public class ClassStart1 {
public static void main(String[] args) {
String firstStudentName = "학생1";
String secondStudentName = "학생2";
int firstStudentAge = 15;
int secondStudentAge = 16;
int firstStudentGrade = 90;
int secondStudentGrade = 80;
System.out.println("이름: " + firstStudentName + " 나이: " + firstStudentAge + " 성적: " + firstStudentGrade);
System.out.println("이름: " + secondStudentName + " 나이: " + secondStudentAge + " 성적: " + secondStudentGrade);
}
}
위 코드에서는 학생 2명을 다루어야 하기 때문에 각각 다른 변수를 사용했습니다.
이 코드의 문제는 학생이 늘어날 때 마다 변수를 추가로 선언해야 하고, 또 출력하는 코드도 추가해야 합니다.
이런 문제를 어떻게 해결할 수 있을까요?
이번에는 위 코드를 “배열을 사용하여 리펙토링” 해봅시다.
아래의 코드는 “배열을 활용한 코드” 입니다.
package class1;
public class ClassStart2 {
public static void main(String[] args) {
String[] studentNames = { "학생1", "학생2" };
int [] studentAges = { 15, 16 };
int [] studentGrades = { 90, 80 };
for (int i = 0; i < studentNames.length; i++) {
System.out.println("이름: " + studentNames[i] + " 나이: " + studentAges[i] + " 성적: " + studentGrades[i]);
}
}
}
이전 코드보다 훨씬 깔끔해졌으며, 위 코드에서 문제가 됐었던 학생이 늘어날 때 마다 변수를 새롭게 추가할 필요도 없어졌습니다.
배열 사용의 한계.
하지만 배열을 사용해서 코드를 최소화하는데 성공했지만 “한 한생의 데이터가 studentNames[], studentAges[], studentGrades라는 3개의 배열에 나누어져 있습니다.”
따라서 “데이터를 변경할 때 매우 조심해서 작업해야 합니다.”
예를 들어서 학생 2의 데이터를 제거하려면 각각의 배열마다 학생2의 요소를 정확하게 찾아서 제거해주어야 합니다.
학생2 제거
String[] studentNames = { "학생1", "학생3", "학생4", "학생5" };
int [] studentAges = { 15, 17, 10, 16 };
int [] studentGrades = { 90, 100, 80, 50 };
한 학생의 데이터가 3개의 배열에 나누어져 있기 때문에 3개의 배열을 각각 변경해야 합니다!!
그리고 한 학생의 데이터를 관리하기 위해 3개의 배열의 인덱스 순서를 항상 정확하게 맞추어야 합니다.(조금이라도 실수하면 😱)
이렇게 하면 특정 학생의 데이터를 변경시 실수할 가능성이 매우 높습니다.
이 코드는 컴퓨터가 볼 때는 아무 문제가 없지만, 사람이 관리하기에는 좋은 코드가 아닙니다. 😵💫
정리
지금처럼 이름, 나이, 성적을 각각 따로 나누어서 관리하는 것은 사람이 관리하기 좋은 방식이 아닙니다.
사람이 관리하기 좋은 방식은 학생이라는 개념을 하나로 묶는 것입니다.
그리고 각각의 학생 별로 본인의 이름, 나이, 성적을 관리하는 것 입니다.
-
☕️[Java] 메서드 파트 정리.
정리
변수명 vs 메서드명
변수 이름은 일반적으로 명사를 사용합니다.
한편 메서드는 무언가 동작하는데 사용하기 때문에 일반적으로 동사로 시작합니다.
이런 차이점 외에는 변수 이름과 메서드 이름에 대한 규칙은 둘다 같습니다.
변수명 예): customerName, totalSum, employeeCount, isAvailable
메서드명 예): printReport(), calculateSum(), addCustomer(), getEmployeeCount(), setEmployeeName()
메서드 사용의 장점
코드 재사용 : 메서드는 특정 기능을 캡슐화하므로, 필요할 때마다 그 기능을 다시 작성할 필요 없이 해당 메서드를 호출함으로써 코드를 재사용할 수 있습니다.
코드의 가독성 : 이름이 부여된 메서드는 코드가 수행하는 작업을 명확하게 나타내므로, 코드를 읽는 사람에게 추가적인 문맥을 제공합니다.
모듈성 : 큰 프로그램을 작은, 관리 가능한 부분으로 나눌 수 있습니다. 이는 코드의 가독성을 향상시키고 디버깅을 쉽게 만듭니다.
코드 유지 관리 : 메서드를 사용하면, 코드의 특정 부분에서 문제가 발생하거나 업데이트가 필요한 경우 해당 메서드만 수정하면 됩니다. 이렇게 하면 전체 코드 베이스에 영향을 주지 않고 변경 사항을 적용할 수 있습니다.
재사용성과 확장성 : 잘 설계된 메서드는 다른 프로그램이나 프로젝트에서도 재사용할 수 있으며, 새로운 기능을 추가하거나 기존 기능을 확장하는 데 유용합니다.
추상화 : 메서드를 사용하는 곳에서는 메서드의 구현을 몰라도 됩니다. 프로그램의 다른 부분에서는 복잡한 내부 작업에 대해 알 필요 없이 메서드를 사용할 수 있습니다.
테스트와 디버깅 용이성 : 개별 메서드는 독립적으로 테스트하고 디버그할 수 있습니다. 이는 코드의 문제를 신속하게 찾고 수정하는데 도움이 됩니다.
따라서, 메서드는 효율적이고 유지 보수가 가능한 코드를 작성하는 데 매우 중요한 도구입니다.
자바에서의 대원칙.
“자바는 항상 변수의 값을 복사해서 대입합니다.”
이 대원칙은 반드시 이해해야 합니다. 그러면 아무리 복잡한 상황에서도 코드를 단순하게 이해할 수 있습니다.
package method;
public class MethodValue0 {
public static void main(String[] args) {
int num1 = 5;
int num2 = num1;
num2 = 10;
System.out.println("num1=" + num1);
System.out.println("num2=" + num2);
}
}
실행 결과
num1 = 5
num2 = 10
용어: 메서드 시그니처(method signature)
메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서)
메서드 시그니처는 자바에서 메서드를 구분할 수 있는 고유한 식별자나 서명을 뜻합니다.
메서드 시그니처는 메서드의 이름과 매개변수 타입(순서 포함)으로 구성되어 있습니다.
쉽게 이야기해서 메서드를 구분할 수 있는 기준입니다.
자바 입장에서는 각각의 메서드를 고유하게 구분할 수 있어야 합니다. 그래야 어떤 메서드를 호출 할 지 결정할 . 수있습니다.
따라서 메서드 오버로딩과 같이 메서드 이름이 같아도 메서드 시그니처가 다르면 다른 메서드로 간주합니다.
반환 타입은 시그니처에 포함되지 않습니다.
-
-
☕️[Java] 메서드.
메서드의 필요성을 알기위해서 아래의 코드를 보고 실제로 느껴보겠습니다.
아래의 코드는 두 숫자를 입력 받아서 더하고 출력하는 단순한 기능을하는 프로그램입니다.
먼저 1+2를 수행하고, 그 다음으로 10 + 20을 수행합니다.
package method;
public class Method1 {
public static void main(String[] args) {
// 계산1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
System.out.println("결과1 출력: " + sum1);
// 계산2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
System.out.println("결과2 출력:" + sum2);
}
}
위 코드는 다음과 같은 특징이 있습니다.
같은 연산을 두 번 수행합니다.
코드를 잘보면 계산 1 부분과, 계산 2 부분이 거의 같습니다.
계산 1
int a = 1;
int b = 2;
System.out.println(a + "+" + b + " 연산 수행");
int sum1 = a + b;
계산 2
int x = 10;
int y = 20;
System.out.println(x + "+" + y + " 연산 수행");
int sum2 = x + y;
계산 1과 2 둘 다 변수를 두 개 선언하고, 어떤 연산을 수행하는지 출력하고, 두 변수를 더해서 결과를 구합니다.
만약 프로그램의 여러 곳에서 이와 같은 계산을 반복해야 할 경우에는 같은 코드를 여러번 반복해서 작성해야 할 것입니다.
더 나아가서 어떤 연산을 수행하는지 출력하는 부분을 변경하거나 또는 제거하고 싶다면 해당 코드를 다 찾아다니면서 모두 수정해야 할 것 입니다.
함수(Function)
함수 정의
add(a, b) = a + b
이름이 add이고, a,b라는 두 값을 받는 함수입니다. 그리고 이 함수는 a + b 연산을 수행합니다.
함수 사용
add(1,2) -> 결과: 3
add(5,6) -> 결과: 11
add(3,5) -> 결과: 8
함수에 값을 입력하면, 함수가 가진 연산을 처리한 다음 결과를 출력합니다. 여기서는 단순히 a + b라는 연산을 수행합니다.
여러번 같은 계산을 해야 한다면 지금처럼 함수를 만들어두고(정의), 필요한 입력 값을 넣어서 해당 함수를 호출하면 됩니다. 그러면 계산된 결과가 나옵니다.
함수는 마치 마술상자와 같습니다. 함수를 호출할 때는 외부에서는 필요한 값만 입력하면 됩니다. 그러면 계산된 결과가 출력됩니다.
내부 구조는 어떻게 되어있는지 알 필요가 없습니다.
같은 함수를 다른 입력 값으로 여러번 호출할 수 있습니다.
여기서 핵심은 함수를 한 번 정의해두면 계속해서 재사용할 수 있다는 점입니다!
평균 함수
만약 두 수의 평균을 구해야 한다면 매번 (a + b) / 2라는 공식을 사용해야 할 것입니다.
이것을 함수로 만들어두면 다음과 같이 사용할 수 있습니다.
함수 정의
avg(a, b) = (a + b) / 2
함수 사용
avg(4, 6) -> 결과: 5
avg(10, 20) -> 결과: 15
avg(100, 200) -> 결과: 150
수하의 함수의 개념을 프로그래밍에 가지고 온다면 어떨까요?
필요한 기능을 미리 정의해두고 필요할 때 마다 호출해서 사용할 수 있기 때문에 앞서 고민한 문제들을 해결할 수 있을 것 같습니다.
프로그램 언어들은 오래 전 부터 이런 문제를 해결하기 위해 수학의 함수라는 개념을 차용해서 사용합니다.
-
-
-
☕️[Java] 메서드(2)
메서드 정의
public static int add(int a, int b) {
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
위 코드가 바로 메서드입니다.
이것을 함수를 정의하는 것과 같이, 메서드를 정의한다고 표현합니다.
메서드는 수학의 함수와 유사하게 생겼습니다.
함수에 값을 입력하면, 어떤 연산을 처리한 다음에 결과를 반환합니다.
수학에 너무 집중하지 않아도 됩니다, 단순히 무언가 정의해두고 필요할 때 불러서 사용한다는 개념으로 이해하면 충분합니다.
메서드는 크게 “메서드 선언” 과 “매서드 본문” 으로 나눌 수 있습니다.
메서드 선언(Method Declaration)
public static int add(int a, int b)
메서드의 선언 부분으로, 메서드 이름, 반환 타입, 파라미터(매개변수) 목록을 포함합니다.
이름 그대로 이런 메서드가 있다고 선언하는 것입니다.
메서드 선언 정보를 통해 다른 곳에서 해당 메서드를 호출할 수 있습니다.
public static
public: 다른 클래스에서 호출할 수 있는 메서드라는 뜻입니다. (접근 제어에서 학습할 예정)
static: 객체를 생성하지 않고 호출할 수 있는 정적 메서드라는 뜻입니다. (자세한 내용은 추후에 정리)
int add(int a, int b)
int: 반환 타입을 정의합니다. 메서드의 실행 결과를 반환할 때 사용할 반환 타입을 지정합니다.
add: 메서드의 이름입니다. 이 이름으로 메서드를 호출할 수 있습니다.
(int a, int b): 메서드를 호출할 때 전달하는 입력 값을 정의합니다. 이 변수들은 해당 메서드 안에서만 사용됩니다. 이렇게 메서드 선언에 사용되는 변수를 영어로 파라미터(parameter), 한글로 매개변수라 합니다.
메서드 본문(Method Body)
{
System.out.println(a + "+" + b + " 연산 수행");
int sum = a + b;
return sum;
}
메서드가 수행해야 하는 코드 블록입니다.
메서드를 호출하면 메서드 본문이 순서대로 실행됩니다.
메서드 본문은 마술상자입니다. 메서드를 호출하는 곳에서는 메서드 선언은 알지만 메서드 본문은 모르기 때문입니다.
메서드의 실행 결과를 반환하려면 return문을 사용해야 합니다. return문은 다음에 반환할 결과를 적어주면 됩니다.
return sum: sum 변수에 들어있는 값을 반환합니다.
메서드 호출
앞서 정의한 메서드를 호출해서 실행하려면 메서드 이름에 입력 값을 전달하면 됩니다. 보통 메서드를 호출한다고 표현합니다.
int sum1 = add(5, 10);
int sum2 = add(15, 20);
메서드를 호출하면 어떻게 실행되는지 순서대로 확인해봅시다.
int sum1 = add(5, 10); // add라는 메서드를 숫자 5, 10을 전달하면서 호출합니다.
int sum1 = 15; // add(5, 10)이 실행됩니다. 실행 결과 반환 값은 15입니다.
메서드를 호출하면 메서드는 계산을 끝내고 결과를 반환합니다.
쉽게 이야기하자면, 메서드 호출이 끝나면 해당 메서드가 반환한 결과 값으로 치환됩니다.
메서드 호출이 끝나면 더 이상 해당 메서드가 사용한 메모리를 낭비할 이유가 없습니다.
메서드 호출이 끝나면 메서드 정의에 사용한 파라미터 변수인 int a, int b는 물론이고, 그 안에서 정의한 int sum도 모두 제거 되기 때문입니다.
메서드 호출과 용어정리
메서드를 호출할 때는 다음과 같이 메서드에 넘기는 값과 매개변수(파라미터)의 타입이 맞아야 합니다.
물론 넘기는 값과 매개변수(파라미터)의 순서와 갯수도 맞아야 합니다.
호출: call("hello", 20)
메서드 정의: int call(String str, int age)
인수(Argument)
여기서 hello,20 처럼 넘기는 값을 영어로 Argument(아큐먼트), 한글로 인수 또는 인자라 합니다.
실무에서는 아규먼트, 인수, 인자라는 용어를 모두 사용합니다.
매개변수(Parameter)
메서드를 정의할 때 선언한 변수인 String str, int age를 매개변수, 파라미터라 합니다.
메서드를 호출할 때 인수를 넘기면, 그 인수가 매개변수에 대입됩니다.
실무에서는 매개변수, 파라미터 용어를 모두 사용합니다.
용어정리
인수라는 용어는 ‘인’과 ‘수’의 합성어로, ‘들어가는 수’라는 의미를 가집니다. 즉, 메서드 내부로 들어가는 값을 의미합니다. 인자도 같은 의미입니다.
매개변수, parameter는 ‘매개’와 ‘변수’의 합성어로 ‘중간에서 전달하는 변수’라는 의미를 가집니다. 즉, 메서드 호출부와 메서드 내부 사이에서 값을 전달하는 역할을 하는 변수라는 뜻입니다.
-
-
-
-
-
☕️[JAVA] Packaing 옵션.
☕️[JAVA] Packaing 옵션.
Spring Initializr에서 "Packaing" 옵션을 선택할 때 'Jar'와 'War' 중에 선택해야 합니다.
어떤 것을 선택해야 할지는 개발하려는 어플리케이션의 유형과 배포 환경에 따라 달라집니다.
각 포맷에 대한 설명.
1️⃣ Jar (Java Archive)
Jar 파일은 Java 클래스 파일, 메타데이터, 리소스 파일을 하나의 파일로 압축한 포맷입니다.
스탠드얼론(Spring Boot 어플리케이션 권장 포맷): Jar 포맷은 내장된 서버(예: Tomcat, Jetty)를 사용하여 스프링 부트 어플리케이션을 스탠드얼론 어플리케이션으로 실행할 수 있게 합니다. 이는 별도의 웹 서버 설치 없이도 실행 가능하며, 마이크로서비스, 클라우드 어플리케이션 개발에 적합합니다.
간편한 배포와 실행: Jar 파일은 'java -jar' 명령어로 쉽게 실행할 수 있으며, 도커 컨테이너와 같은 환경에 배포하기도 용이합니다.
2️⃣ War (Web Application Archive)
War 파일은 웹 어플리케이션에 필요한 Java 클래스 파일, JSP(JavaServer Pages), 서블릿, 리소스 파일, 메타데이터 등을 포함한 포맷입니다.
전통적인 웹 어플리케이션: War 포맷은 서블릿 컨테이너나 어플리케이션 서버(예: Tomcat, Jetty, WebLogic, WildFly)에 배포될 전통적인 웹 어플리케이션 개발에 사용됩니다. 이 경우, 어플리게이션 서버가 웹 어플리케이션을 실행하는데 필요한 환경을 제공합니다.
엔터프라이즈 환경: 복잡한 엔터프라이즈 환경에서는 여러 어플리케이션을 하나의 서버에 배포해야 할 필요가 있을 수 있으며, War 포맷이 이러한 요구 사항을 충족시킬 수 있습니다.
🙌 선택 기준
스탠드얼론 어플리케이션 개발 및 마이크로 아키텍처를 선호한다면 'Jar'를 선택하세요.
기존의 엔터프라이즈 환경에서 어플리케이션 서버를 사용해야 한다면 'War'를 선택하세요.
Spring Boot는 두 가지 포맷 모두를 지원하므로, 프로젝트 요구 사항과 배포 환경에 맞게 최적의 옵션을 선택할 수 있습니다.
-
Touch background to close