최근 코드리뷰간 다음 질문을 받았습니다.

for loop 문 말고 stream api 를 사용해보시는 건 어떨까요?

저는 평소에 함수형 프로그래밍과 Stream API에 문외한이었어서 해당 리뷰를 받고 'stream 이 훨씬 좋은 거구나!' 라고 생각하게 되어  

이 일을 계기로 함수형 프로그래밍과 람다, Stream API에 대한 공부를 진행했습니다.

 

위 개념은 흥미롭고 재미있어서 여러 상황에서 사용해보려 시도하고 기존 for-loop 사용할 때와 가독성이 어떻게 변했는지 비교해봤던것 같습니다. 공부를 하다보니 문득 '진짜 stream이 for-loop 보다 좋을까?' 하는 의문이 들었고 두 가지를 비교한 글들을 찾아 해당 내용을 정리해 공유해보는 글을 작성했습니다.

 


주요 비교 사항

  • 성능 (Performance)
  • 가독성 (Readability)

성능

사례 1. 500000개의 primitive type 

// for-loop
int[] a = ints;
int e = ints.length;
int m = Integer.MIN_VALUE;
for (int i = 0; i < e; i++) {
    if (a[i] > m) {
        m = a[i];
    }
}// sequential stream
int m = Arrays.stream(ints).reduce(Integer.MIN_VALUE, Math::max);

 

primitive type 같은 경우, for-loop가 큰 차이의 성능으로 stream을 압도하는 것을 확인하실 수 있습니다.

 

사례 2. 500000개의 Wrapper Class

Wrapper Class의 경우 성능 차이가 크게 발생하지 않는데, 이는 ArrayList의 iteration이 훨씬 속도에 큰 영향을 미치기 때문입니다.

 

사례 3. 10000개의 요소를 반복하며 시간이 꽤 소요되는 기능을 호출할 경우

// for-loop
int[] a = ints;
int e = a.length;
double m = Double.MIN_VALUE;for (int i = 0; i < e; i++) {
     double d = Sine.slowSin(a[i]);
     if (d > m) m = d;
}// sequential stream
Arrays.stream(ints).mapToDouble(Sine::slowSin).reduce(Double.MIN_VALUE, Math::max);

더 이상 시간 차이가 크게 발생하지 않는 것을 확인할 수 있습니다.

 

세 가지의 케이스를 통해 for-loop가 성능은 빠르지만, 두 가지의 성능 비교는 서비스 로직에서 큰 영향을 미치지 않는다라는 결론이 도출되었습니다.

primitive type과 wrapper class 반복문의 경우, 참조 속도에 따른 시간 소요가 for-loop, stream간의 성능 차이보다 훨씬 더 많이 소요된다는 것을 알 수 있습니다. 또한, Sine.slowSin과 같은 계산 비용이 큰 메소드를 사용할 경우 for-loop와 stream간의 성능 차이는 발생하지 않는 것을 확인했습니다. 

 

사실상 for-loop와 stream의 성능 비교를 하는 것은 무의미하다라는 결론을 낼 수 있죠.

 


가독성

다음 두 가지의 코드는 동일한 기능을 제공합니다. 복잡도가 n인 반복문 예제를 한 번 읽어보시죠.

for-loop

private void forLoop() {
    List<Person> persons = List.of();
    List<String> result = new ArrayList<>();
    for(Person p : persons){
        if(p.getAge() > 18){
            result.add(p.getName());
        }
    }
}

Stream

private void streaming() {
    List<Person> persons = List.of();
    List<String> result = persons.stream()
            .filter(p -> p.getAge() > 18)
            .map(p -> p.getName())
            .collect(Collectors.toList());

 

이렇게 봤을 경우 그렇게 큰 차이가 없어보입니다. Stream을 처음 접하시는 분들은 오히려 for-loop로 작성한 코드가 더 익숙할 수 있고요.

for-loop와 stream 코드를 비교하면 다음과 같습니다.

  for-loop Stream
조건 if(p.getAge() > 18) filter(p->p.getAge() > 18)
매핑 p.getName() map(p-> p.getName())
수집 collect(Collectors.toList()); result.add(p.getName());

그렇다면 조건문이 좀 더 복잡해지면 어떨까요?

for-loop

// 조건이 더 복잡해진 코드
private void forLoop(){
    List<Person> persons = List.of();
    List<String> result = new ArrayList<>();

    for(Person p : persons){
        if(p.getAge() > 18 && p.getAge() <= 65 && p.getName() != null && p.getName().startsWith("B")){
            result.add(p.getName());
        }
    }
}
// forLoop()와 동일한 로직을 구현한 코드, 
private void forLoop2() {
    List<Person> persons = List.of();
    List<String> result = new ArrayList<>();

    for (Person p : persons) {
        if (p.getAge() > 18 && p.getAge() <= 65) {
            if (p.getName() != null && p.getName().startsWith("B")) {
                result.add(p.getName());
            }
        }
    }
}

Stream

private void streaming() {
    List<Person> persons = List.of();
    List<String> result = persons.stream()
            .filter(p -> p.getAge() > 18)
            .filter(p -> p.getAge() <= 65)
            .filter(p -> p.getName() != null)
            .filter(p -> p.getName().startsWith("B"))
            .map(p -> p.getName())
            .collect(Collectors.toList());
}

다음 케이스를 보시면 for문이 복잡해질수록 stream이 읽기에 좀 더 편하다는 것을 느끼실 수 있습니다.

stream은 각각의 역할에 따라 filter, map, collect등 다양한 기능을 제공하고 있기 때문에 for문 내에서 진행되는 로직의 역할을 기능적으로 분리해 이를 구현할 수 있기 때문입니다. 

 


결론

  • 성능은 for-loop가 중요하지만 생각보다 큰 차이는 발생하지 않습니다.
  • primitive type과 같은 직접 참조에 있어서는 for-loop가 성능이 월등하게 뛰어납니다.
  • Wrapper Type에서는 간접 참조여서 heap에서 주소를 확인하는 시간이 시간이 더 오래 소요되기 때문에 큰 성능 차이가 발생하지 않습니다.
  • 반복문에 들어가는 데이터의 모수가 적을수록 for-loop의 성능이 좋으며, 데이터가 커질수록 parallelStream의 성능이 좋아집니다.
  • 가독성은 반복문 내 로직이 복잡해질수록 Stream의 가독성이 훨씬 뛰어납니다.

성능도 물론 서비스 로직을 구현할 때 중요한 요소이지만, 미미한 성능 차이라면 유지 보수를 위해 가독성 있는 코드를 작성하는 것에 지향점을 두어야 된다고 생각합니다.

 

따라서, 가능한 stream을 통한 구현을 하되, 간단한 로직에서는 익숙한 방법을 사용해도 좋을 것 같습니다. 다만, for-loop와 stream에 대해 상황에 따라 다르게 사용한다면 유지보수간 불편함을 느낄 수 있기 때문에 해당 부분은 팀 내 컨벤션을 만들어 공유하는게 어떨까하는 의견을 드립니다. 

 

감사합니다.

Reference

https://blog.jdriven.com/2019/10/loop/

 

Java streams vs for loop

Java streams vs for loop I had quite a bit of trouble finding a good article about java streams vs for loops under this name so I guess I’ll have to write it myself. In this article I would li

blog.jdriven.com

https://sigridjin.medium.com/java-stream-api%EB%8A%94-%EC%99%9C-for-loop%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B4%EA%B9%8C-50dec4b9974b

 

Java Stream API는 왜 for-loop보다 느릴까?

The Korean Commentary on ‘The Performance Model of Streams in Java 8" by Angelika Langer

sigridjin.medium.com

 

이번 글은 회사 업무중 발생한 이슈 상황에 대해 공유해보고자 합니다. 

문제 상황

저는 현재 Email 서비스를 담당하여 서비스 개발을 진행하고 있습니다.

 

문제가 발생한 것은 4월 25일 (화)요일 회사 서비스 정기점검이었습니다. 서비스 개선 사항을 배포한 뒤 고객사로부터 Email SMTP 서비스로 발송이 되지 않는다는 문의를 받게 되었습니다. 주된 문의는 TLS 버전 관련 에러 로그가 발생하며 메일 발송이 실패처리가 된다는 것이었습니다.

 

타임라인

4월 25일
06:00: 서비스 배포 시작
09:00: 배포 및 서비스 확인 완료 
12:20: 고객사 문의 (Email SMTP 서비스 발송 실패 현상)
12:30: 서비스 롤백 (고객사 재확인시 정상 발송됨을 확인)
13:30: 장애 원인 확인이 어려워 다시 개선 버전으로 배포 (고객사 재확인시 이슈 없다고 답변)
13:40: 이슈 대응 종료

4월 26일
13:40: 다른 고객사 문의 (Email SMTP 서비스 발송 실패 현상)
- javax.mail.MessagingException: Could not convert socket to TLS; 메세지 발생
- prop.put("mail.smtp.ssl.protocols", "TLSv1.2"); 버전 명시 후 정상 발송
17:00: 관련 원인 추가 파악 진행 (원인 파악)

4월 27일 
14:00: 고객사에 실패 발송 코드 및 사용 jdk 버전, 사용중인 mail 라이브러리 공유 요청
15:00: 구체적인 이슈 파악 완료 후 공유 

원인 파악

원인 파악에 있어 고려했던 요소들은 다음과 같습니다.

1. 서비스 배포간 큰 변경 사항이 있었는가?

큰 특이사항은 두 가지가 있었습니다.

  • jdk 버전이 openjdk 8u242b08에서 openjdk 8u362b09 버전으로 업데이트 되었습니다.
  • NettyServer를 그대로 사용할 경우 서비스 내부적으로 사소한 이슈가 있어 클래스를 상속받아 customNettyServer를 구현했습니다.

2. 서비스 전체가 실패하는가?

배포 후 특이했던 부분은 서비스 운영간 TLS 버전을 명시하지 않을 경우 실패한다는 점이었습니다. TLS 버전을 명시하면 정상 발송이 됐었기에 서비스를 롤백하지 않아도 됐었다는 점은 아쉬운 대응으로 남아 아래 회고에 추가로 작성했습니다.

 

장애 원인이 있을만한 지점으로는 크게 두 가지였으나 CustomNettyServer의 경우 상속만 받았을 뿐 큰 개선사항이 없었기에 해당 장애를 유발시킬 수 없다는 판단이 되어 jdk 버전간 변경사항을 확인하게 되었습니다.

 

 

8u242b08에서 8u362b09 개선사항

생각보다 jdk 8로 백포트된 업데이트 사행이 몇가지 존재했습니다. 이 중 TLS 관련 개선 사항만을 확인했습니다.

 

https://bugs.openjdk.org/browse/JDK-8257122

 

[JDK-8257122] Disable TLS 1.0 and 1.1 - Java Bug System

Summary This is a CSR based on JDK-8254713. It's been opened for the Oracle JDK update releases: 11.0.11 8u291 7u301 Same approach as that of JDK 16 is being taken. Disable the TLS 1.0 and 1.1 protocols by default. Problem TLS 1.0 and 1.1 are versions of t

bugs.openjdk.org

https://bugs.openjdk.org/browse/JDK-8270344

 

[JDK-8270344] Session resumption errors - Java Bug System

Summary of submitter's issue: In a TLS session resumption scenario, the connection fails in following scenario if using JDK 7u/6u Server started and configured to use TLSv1 only Client started and sends ClientHello with TLSv1.2 request Server responds with

bugs.openjdk.org

https://bugs.openjdk.org/browse/JDK-8245263

 

[JDK-8245263] Enable TLSv1.3 by default on JDK 8u for Client roles - Java Bug System

TLSv1.3 implementation is available in JDK 8u from 8u261 and default disable on client roles. After observing inflow from field, we will enable it by default for client roles.

bugs.openjdk.org

세가지 이슈를 토대로 다음과 8u362b09로 버전이 업데이트되며 같은 개선 사항이 있음을 확인했습니다.

  • TLS1.0 TLS1.1은 사용이 불가능하다.
  • TLS 세션 재개 시나리오에선 TLSv1이 포함될 경우 요청을 거부합니다.
  • TLS1.3이 Client default TLS 버전이 되었습니다.

그렇다면 에러 케이스는 어떻게 발생한 것일지 확인이 필요했습니다. 개선사항을 확인했을 경우 TLS1.0 TLS1.1로 통신을 했거나, TLS 세션 재개 시나리오에서 TLSv1을 사용했을 확률이 높아졌습니다.

 

추가로 관련 내용을 확인해보니 다음 이슈 두가지로 원인이 확실해졌습니다.

https://jira.atlassian.com/browse/FE-7294

 

[FE-7294] Update JavaMail library from 1.4 to 1.6 in order to support TLS 1.2 - Create and track feature requests for Atlassian

Problem Definition The old JavaMail 1.4 jar must use TLS 1.0 or 1.1, as can be seen here. The problem is that most companies have disabled TLS 1.0 and 1.1 for well known security problems. Adding -Dmail.smtp.ssl.protocols=TLS1.2 to FISHEYE_OPTS environment

jira.atlassian.com

https://shanepark.tistory.com/426

 

[Java Mail] Could not convert socket to TLS; 문제 해결

문제 서버에서 javax email을 활용해 구글 이메일을 전송 할 때 아래와 같은 에러가 발생 했습니다. org.springframework.mail.MailSendException: Mail server connection failed; nested exception is javax.mail.MessagingException: Co

shanepark.tistory.com

 

결론

현재 jdk 업데이트를 통해 보안성이 낮은 TLSv1.0, TLSv.1.1을 제거했으며 TLSv1.2 또는 TLSv1.3을 사용하는 것을 권장하도록 개선된 것이기에 배포된 버전이 좀 더 보안적으로 향상되어 발생한 이슈였습니다.

 

문제가 발생한 고객사의 dependency를 확인한 결과 java-mail 1.4.7을 사용하고 있었는데 java-mail 1.4.7은 default TLS 버전으로 TLSv1.0을 사용하고 있습니다.  java-mail 1.6 버전부터는 TLSv1.2가 default TLS이기에 이슈가 없을 것으로 확인됩니다.

 

해결 방안은 다음과 같습니다.

  • 라이브러리 별 Default TLS 버전을 확인하고 default가 TLSv1.0, TLSv.1.1일 경우 버전을 명시해 사용한다.
    • props.put("mail.smtp.ssl.protocols", "TLSv1.2");
  • jdk 버전을 낮춘 상태로 유지한다.

회고

쉬운 내용에도 불구하고 원인 파악에 오래걸린 이유는 다음과 같습니다.

  • 4월 25일에 롤백 -> 재배포간 고객사에서 정상 발송이 된다고 답변받음 (서비스에 이슈가 없다고 파악해 원인 파악에 있어 하루 지연)
  • Email SMTP 코드 내에서 자체적으로 통신 테스트를 재현해봤으나, 정상적인 통신이 되어 문제 상황 파악 불가능
    • jakarta 1.6.5와 java-mail 1.4.7을 사용할 경우 기본으로 jakarta 1.6.5이 사용되는데  jakarta 1.6.5는 TLS 1.2을 사용하고 있어 문제상황 재현이 불가능
    • 인지후 새 프로젝트를 만들어 재현 

 

 

 

 

'Java > 개념' 카테고리의 다른 글

Stream API - 개념  (0) 2023.04.10
Heap Pollution(힙 오염)  (0) 2023.04.08
Java의 인터페이스는 왜 다른 인터페이스를 구현할 수 없을까?  (0) 2023.03.20
9 java best practices  (0) 2023.03.11

Stream 이란?

Java 8 부터는 java.util.stream이라는 새로운 추가 패키지를 제공합니다. 이 패키지는 Collection을 처리하는데 사용되며, 원하는 결과를 도출해내기 위해 다양한 메소드를 파이프라인으로 연결해 사용할 수 있는 일련의 개체입니다.

 

Stream의 특징

  1. Stream은 데이터를 보유하지 않습니다. (자료 구조 X)
  2. Stream은 본질적으로 Functional합니다. Stream에서 수행되는 작업은 소스를 수정하지 않습니다.
  3. Stream은 필요한 경우에만 동작하며 소스의 요소들은 작업이 시작될 때만 소비되며 수행됩니다. (Lazy)
  4. Stream의 생애 주기(lifecycle)은 사용되는 시점 단 한 번만 사용됩니다. Iterator와 마찬가지로 동일한 요소를 다시 방문하기 위해서는 새로운 Stream을 생성해야 합니다.

Stream 기능

Stream은 크게 두 가지 기능으로 분류할 수 있습니다.

 

Intermediate Operation

Intermediate Operation은 Stream을 다른 Stream으로 변환하는 작업입니다. 이러한 작업은 최종 결과나 출력을 생성하지 않는 대신 추가로 처리하거나 다른 목적으로 사용할 수 있는 새로운 스트림을 생성합니다.

map

map 메소드는 작성한 메소드를 스트림 요소에 적용한 결과로 반환하는데 사용합니다.

List number = Arrays.asList(2,3,4,5);
List square = number.stream().map(x->x*x).collect(Collectors.toList());
//예상 결과값 
// 4, 9, 16, 25

filter

filter 메서드는 인수로 전달된 Predicate에 따라 요소를 선택하는데 사용합니다.

List names = Arrays.asList("Reflection","Collection","Stream");
List result = names.stream().filter(s->s.startsWith("S")).collect(Collectors.toList());
//예상 결과 "Stream"

sorted

sorted 메서드는 Comparator에 따라 Stream 요소들을 정렬하는데 사용됩니다. 

List names = Arrays.asList("Reflection","Collection","Stream");
List result = names.stream().sorted().collect(Collectors.toList());

 

 

Terminal Operation

Terminal Operation은 최종 결과 또는 출력을 생성하는 작업입니다.

collect

collect는 Stream에서 수행된 중간 작업의 결과를 반환하는데 사용됩니다.

List number = Arrays.asList(2,3,4,5,3);
Set square = number.stream().map(x->x*x).collect(Collectors.toSet());

forEach

forEach 메서드는 Stream의 모든 요소를 반복하는데 사용됩니다.

List number = Arrays.asList(2,3,4,5);
number.stream().map(x->x*x).forEach(y->System.out.println(y));

reduce

reduce는 BinaryOperator를 매개변수로 사용해 Stream의 요소를 하나의 값으로 줄이는 데 사용됩니다. 

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

System.out.println(sum); // Output: 15

결론 

Java 8부터는 Collection을 작업하는 새로운 기능으로 Stream API를 제공하기 시작했습니다. Lazy함과 병렬 처리, 가독성 등 다양한 이점을 가지고 있는 Stream API는 JDK 8 출시 이후 현재까지 많은 사랑을 받고 있습니다.

 

Stream API는 크게 Intermediate Operation과 Terminal Operation으로 나뉘어져 있습니다. 두 가지의 주요 차이점은 Intermediate Operation Stream 처리를 유발하지 않고 대신 추가 처리에 사용할 있는 Stream을 생성하는 반면 Terminal Operation Stream 처리를 트리거하고 최종 결과 또는 출력을 생성합니다.

 

지금까지 Stream의 특징과 대표적인 기능들에 대해 알아봤습니다. Stream API의 Intermediate Operation과 Terminal Operation은 해당 글에서 설명한 기능 외에도 더 많은 기능들이 제공하니 공식 문서를 한 번 읽어보시는 것을 권장드립니다.

 

다음 글에서는 지금 배운 Stream을 어떻게 사용하는지, 사용할 경우 어떤 trade-off가 발생할 수 있을지에 대해 이야기해보겠습니다.

 

감사합니다. 

Reference

Interface Stream
The Java 8 Stream API Tutorial
Stream In Java

Heap Pollution이란?

Heap Pollution이란,  JVM의 힙 영역(heap area)이 오염된 상태를 의미합니다. 위키피디아에서는 Heap Pollution에 대해 다음과 같이 정의하고 있습니다. 

In the Java programming language, heap pollution is a situation that arises when a variable of a parameterized type refers to an object that is not of that parameterized type. This situation is normally detected during compilation and indicated with an unchecked warning. Later, during runtime heap pollution will often cause a ClassCastException.

A source of heap pollution in Java arises from the fact that type arguments and variables are not reified at run-time. As a result, different parameterized types are implemented by the same class or interface at run time. All invocations of a given generic type declaration share a single run-time implementation. This results in the possibility of heap pollution.

여기서  when a variable of a parameterized type refers to an object that is not of that parameterized type. 문구에 집중할 필요가 있습니다.

 

파라미터화된 타입이 파라미터화되지 않은 타입에 추론될 경우 Heap Pollution이 발생하며, 컴파일간 unchecked warning으로 인식되기 때문에 런타임간 ClassCastException을 야기할 수 있습니다. 그렇다면 어떤 상황에서 Heap Pollution이 발생할까요?

 

Heap Pullution이 발생할 수 있는 경우

- Generic - Java 5

- varargs parameter - Java 5

 

Generic은 하나 이상의 타입을 받을 수 있도록 만들어진 개념이며, varargs는 인수의 갯수를 개발자가 조절할 수 있게 도와주는 개념입니다. 

varargs 오류 예시

public class Example {
  public static void main(String[] args) {
    List<String> stringList = new ArrayList<>();
    stringList.add("hello");
    stringList.add("world");

    addToList(stringList, 1, 2, 3);
    
    System.out.println(stringList);
  }
  
  public static <T> void addToList(List<T> list, T... elements) {
    for (T element : elements) {
      list.add(element); //String과 Integer를 하나의 리스트에 add하여 오류 발생
    }
  }
}

Generic 오류 예시

import java.util.List;
import java.util.ArrayList;

public class Example {
  public static void main(String[] args) {
    List<String> stringList = new ArrayList<>();
    stringList.add("hello");
    stringList.add("world");
    
    List<Integer> integerList = new ArrayList<>();
    integerList.add(1);
    integerList.add(2);
    
    List<Object> objectList = new ArrayList<>();
    addToList(objectList, stringList);
    addToList(objectList, integerList);
    
    System.out.println(objectList);
  }
  
  public static <T> void addToList(List<T> list, List<? extends T> elements) {
    list.addAll(elements); //컴파일간 이상 없으나 String과 Integer라는 다른 형을 add하는 과정에서 런타임 오류 발생
  }
}

 

그렇다면 Heap Pollution을 방지하는 방법에는 무엇이 있을까요?

Heap Pollution 해결 방법

  • 가능한 타입에 저장, 수정 처리를 하지 않는다. (해야 될 경우, 신중하게 사용할 것을 권장)
  • 와일드 카드를 사용해 동일한 유형인지 검증하도록 구현한다.

varargs 오류 해결

//변경 전
public static <T> void addToList(List<T> list, T... elements) {
  for (T element : elements) {
    list.add(element);
  }
}

//변경 후
public static <T> void addToList(List<T> list, Class<T> type, T... elements) {
  for (T element : elements) {
    if (type.isInstance(element)) {
      list.add(element);
    }
  }
}


addToList(stringList, String.class, "hello", "world");
addToList(stringList, String.class, "foo", "bar");

Generic 오류 해결 

import java.util.List;
import java.util.ArrayList;

public class Example {
  public static void main(String[] args) {
    List<String> stringList = new ArrayList<>();
    stringList.add("hello");
    stringList.add("world");
    
    List<Integer> integerList = new ArrayList<>();
    integerList.add(1);
    integerList.add(2);
    
    List<Object> objectList = new ArrayList<>();
    addToList(objectList, stringList);
    addToList(objectList, integerList);
    
    System.out.println(objectList);
  }
  
  public static <T> void addToList(List<T> list, List<? extends T> elements) {
    list.addAll(elements);
  }
}

 

 

 

 

 

코딩 인터뷰 관련 칼럼을 읽는 도중 재미있는 내용이 있어 발췌하였습니다.

 

질문 내용: 왜 인터페이스는 인터페이스를 구현하지 못할까요?

이 질문이 무엇을 의미하는지 좀 더 구체화하기 위해 예제 코드를 작성하겠습니다. 아래 코드에서 어떤 코드가 실제로 동작하지 않을까요?

interface A {
    void display();
}
abstract class B implements A {
}
class C extends B {
   void display(){
   }
}
interface D implements A {
}
더보기

정답은... 4번 입니다!

 

그렇다면 어떠한 이유에서 인터페이스는 인터페이스를 구현할 수 없을까요?

 

이 질문에 대한 답을 하기 위해서는 가장 먼저 추상화인터페이스의 개념에 대해 정확하게 이해하고 있어야 합니다.

 

Java의 추상화

추상화란, 사용자에게 기능을 제공할 때 세부 정보를 숨기고 기능만 표시하는 과정입니다.

 

메세지를 전송한다고 했을때 우리는 일반적으로 핸드폰을 켜서, 텍스트를 적고 이를 보내고 싶은 대상을 선택하여 전송합니다. 다음과 같이 메세지 전송이라는 기능의 세부 정보"메세지를 전송한다"로만 표현하는 과정을 추상화라고 부릅니다.

 

Java에서는 추상화를 통해 각 객체가 어떻게 기능을 수행하는지보다 무엇을 어떤 기능을 수행할 수 있는지에 초점을 두고 개발할 수 있도록 도움을 줍니다.

 

Java에서 추상화를 달성하는 방법에는 두 가지가 존재합니다.

  • 추상 클래스(abstract class)
  • 인터페이스(interface)

추상 클래스란?

추상 클래스는 다음과 같은 특징이 존재합니다.

  • 추상 클래스의 인스턴스는 추상 클래스에 메서드 정의가 없는 추상 메서드를 가질 수도 있기 때문에 인스턴스화가 불가능합니다.
  • 추상 클래스의 데이터 필드를 초기화할 수 있도록 생성자가 허용됩니다.
  • 추상 메서드 없이 추상 클래스를 가질 수 있습니다.
  • 추상 클래스에서 정적 메서드를 정의할 수 있습니다.
package com.example;

abstract class Bank {
    int roi;

    public Bank(int roi) {
        this.roi = roi;
    }

    //abstract method
    abstract float getROI();

    //non-abstract method
    void writeMessage() {
        System.out.println("in writeMessage of Bank class.");
    }

    //final method
    final void sayHello() {
        System.out.println("in sayHello of Bank class.");
    }

    //static method
    static void display() {
        System.out.println("in display of Bank class.");
    }
}

class CentralBank extends Bank {
    String name;

    public CentralBank(int roi, String name) {
        super(roi);
        this.name = name;
    }

    @Override
    void writeMessage() {
        System.out.println("in writeMessage of CentralBank class.");
    }

    @Override
    float getROI() {
        System.out.println("in display of CentralBank class.");
        return 6.5f;
    }
}

public class Main {

    public static void main(String[] args) {
        Bank bank = new CentralBank(10, "CentralBank");
        bank.getROI();
        bank.writeMessage();
        bank.sayHello();
        Bank.display();
    }
}

 

인터페이스

인터페이스는 클래스의 청사진입니다. 클래스는 인터페이스를 통해 어떤 기능을 구현할 것인지 고민할 수 있게 도움을 줍니다.

 

인터페이스의 특징

  • 인터페이스를 통해 다중 상속 기능을 지원할 수 있습니다.
  • 느슨한 결합을 달성할 수 있습니다.
  • 추상 클래스처럼 인스턴스화 할 수 없습니다.
package com.example;

interface Shape {
    void draw();

    default void display() {
        System.out.println("default method");
    }
}


class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("drawing rectangle");
    }

    @Override
    public void display() {
        System.out.println("overridden default method");
        Shape.super.display();
    }
}

public class Main {

    public static void main(String[] args) {
        Shape shape = new Rectangle();
        shape.display();
        shape.draw();
    }
}

 

간단하게 추상 클래스와 인터페이스의 특징과 코드를 살펴보았습니다. 이제 위의 질문에 답할 수 있는 포인트를 찾아야겠죠. 

과연 어떠한 이유로 인터페이스는 인터페이스를 구현할 수 없을까요?

 

결론

다음 세가지 특이사항을 확인해봅시다.

  • 클래스는 다른 클래스를 상속받거나 인터페이스를 구현해 추상 메서드에 대한 메서드 정의를 제공할 수 있습니다.
  • 다른 인터페이스를 확장하는 인터페이스는 자신의 추상 메서드만 추가하고 다른 인터페이스의 추상 메서드에 대한 메서드 정의를 제공하고 있지 않기 때문에 인터페이스는 다른 인터페이스를 확장한다, 
  • 인터페이스는 메서드 정의를 제공하지 않으므로 인터페이스는 클래스를 확장하지 않습니다.

요약하자면 인터페이스는 오로지 추상 메서드만 추가할 수 있으며, 이미 메서드가 정의된 클래스를 상속받을 수 없음을 알 수 있습니다.

추상 메서드만을 가질 수 있는 인터페이스에게 구현을 요구하는 순간 이는 잘못된 것이라는 것이죠. 따라서 인터페이스에게는 구현을 요구하는 implements는 사용할 수 없는 것이죠. 

 

'Java > 개념' 카테고리의 다른 글

Java JDK 8 버전 업데이트간 TLS 버전으로 인한 발송 실패 이슈 회고  (0) 2023.04.28
Stream API - 개념  (0) 2023.04.10
Heap Pollution(힙 오염)  (0) 2023.04.08
9 java best practices  (0) 2023.03.11

이 글은 20 java best practices 를 참고해 작성한 글입니다.

 

이 칼럼에서는 20가지의 대표 예제를 작성했지만, 주관적인 의견을 포함하여 일부 사례만 요약해 작성해 볼 예정입니다.

 

1. 홀수를 확인할 땐 AND operator가 modulo operator보다 빠릅니다.

public boolean isOdd(int num) {
return (num & 1) != 0;
} 
// best way to check the oddity of a number

 

2. 중복 초기화 방지 

default 값으로 초기화되는 변수들을 다시 한 번 초기화 하지 않는 것을 권장합니다.

String name = null; // 중복!
int speed = 0; // 중복!
boolean isOpen = false; // 중복!


String name; 
int speed;
boolean isOpen;

3. new 키워드를 이용한 String 객체 생성을 피하세요.

String 객체는 String Pool이 존재하여 리터럴로 작성할 경우 해당 풀에서 참조가 가능하나, new 키워드로 생성할 경우 새로운 heap 영역을 할당합니다.

String s1 = new String("AnyString") ; // bad : slow instantiation
// The constructor creates a new object, and adds the literal to the heap


String s2 = "AnyString" ; // good: fast instantiation
// This shortcut refers to the item in the String pool 
// and creates a new object only if the literal is not in the String pool.

 

4. String을 이어붙일경우, + operator가 아닌 StringBuilder 혹은 StringBuffer를 사용하세요.

+ 연산자는 중간 연산 과정에서 계속해서 새로운 객체를 생성하며 결과물을 만들지만 StringBuilder와 StringBuffer는 중간 과정에서 String 객체를 생성하지 않습니다.

 

String address = streetNumber +" "+ streetName +" "
+cityName+" "+cityNumber+" "+ countryName; // bad


StringBuilder address = new StringBuilder(streetNumber).append(" ")
.append(streetName).append(" ").append(cityName).append(" ")
.append(cityNumber).append(" ").append(countryName); // good

 

※ 주의사항: StringBuilder는 thread safe하지 않으며 StringBuffer는 thread safe합니다. 

성능상 StringBuilder가 뛰어나나, 다중 스레드 환경에서는 사용하지 않는 것을 권장합니다.

 

5. try-catch-finally에서 try-with-resources로 변경하세요.

Scanner scanner = null;
try {
	scanner = new Scanner(new File("test.txt"));
	while (scanner.hasNext()){
    	System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
	e.printStackTrace();
} finally {
	if (scanner != null) {
    	scanner.close();
	}
}
// error-prone as we can forget to close the scanner in the finally block



try (Scanner scanner = new Scanner(new File("test.txt"))) {
	while (scanner.hasNext()) {
    	System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
	fnfe.printStackTrace();
} 
// cleaner and more succinct

 

6. 가능한 NPE(NullPointerException을 피하세요.

권장사항 모음

  • Null 대신 Empty Collection을 반환
  • 가능하면 Optional을 사용
  • java.utils.Objects의 requireNonNull 메소드를 사용
  • NotNull, NotEmpty, NotBlank 어노테이션을 사용
  • Streams에서는 Objects::nonNull을 사용
  • java.util.Objects의 requireNonNullmethods를 사용
 

7. Lombok을 피하고 필요한 getter,setter, constructor만 추가해 사용하세요

Lombok은 일부 상용구 코드를 생성하는데 도움이 될 수 있는 라이브러리입니다. Java compiler와 밀접한 연관이 있는데, IDE 비호환성, 비공개 API 사용과 같은 몇 가지 단점이 존재해 필요한 함수만 정의하여 사용하는 것을 권장합니다.

8. 상수는 인터페이스 대신 enum 또는 final class를 이용하세요.

public  final  class  MyValues ​​{ 
  private  MyValues ​​() { 
    // 클래스를 인스턴스화할 필요가 없으며 생성자를 숨길 수 있습니다.
   } 
  public  static  final  String  VALUE1  =  "foo" ; 
  공개  정적  최종  문자열  VALUE2  =  "bar" ; 
}

9. static field는 클래스의 맨 위에 배치시키세요.

 

 

 

+ Recent posts