자바 인터페이스와 추상클래스

Dope
Webdev TechBlog
Published in
10 min readOct 1, 2019

서론

이번 주제는 인터페이스와 추상클래스 그리고 스프링에서 인터페이스를 사용하는 이유와 Service와 ServiceImpl에 대한 고찰 및 의존성 주입에 대해서 짧게 소개하려고합니다. 개인적인 주관이 담겨있을 수도 있고 부족한 내용도 있을 수 있으니 참고하시고 읽어주시면 감사하겠습니다.

인터페이스(interface)

인터페이스란 상수(static final)와 추상 메서드(abstract method)의 집합입니다. 생성자를 가질 수 없으며 따라서 객체화가 불가능합니다. 인터페이스에 선언된 상수와 추상 메서드는 public static finalpublic abstract 를 생략해도 되는데 이유는 컴파일 시에 자동으로 생성해주기 때문입니다. 인터페이스는 다중상속을 지원하며, 구현체에 여러개의 인터페이스를 구현할 수 있습니다.

인터페이스를 이해하는데 가장 중요한 영어 숙어는 be able to 입니다. 즉, “~할 수 있는” 이라는 의미를 가지고 있습니다. 그렇기 때문에 인터페이스 네이밍 규칙이 있는데 보통 xxxable 이런 형식으로 짓습니다. 구현체 네이밍은 xxxImpl 과 같이 클래스 이름을 짓습니다. 구현체란 인터페이스를 구현한 클래스라는 뜻이며, 구현 클래스 혹은 실체 클래스 라고도 부릅니다.

자바8 부터 인터페이스에 디폴트 메서드(default method)를 지원합니다. 아래는 프린트 할 수 있는(Printable) 이라는 의미를 가진 인터페이스 입니다.

디폴트 메서드의 목적은 기존 인터페이스 기능을 확장하며, 구현체에 공통적으로 들어갈 기능(코드)를 디폴트 메서드 내부에 작성함으로써 반복되는 코드의 작성을 줄여줍니다. 특징은 default 키워드를 반드시 붙여줘야합니다.따라서 나중에 setPrint()의 기능이 추가되었을때 구현체에서 고칠 필요가 없으며 인터페이스의 디폴트 메서드 내부 코드만 수정하면됩니다.

추상 클래스(abstract class)

추상 클래스는 필드, 생성자, 추상메서드를 가질 수 있고, 인터페이스와 다르게 필드도 가질 수 있습니다. 아래 질문에 대한 답을 한번 생각해보시기 바랍니다.

추상 클래스는 하위 클래스들의 공통점들을 추상화하여 만든 클래스이기 때문에 추상 클래스 자체를 new 연산자를 통하여 객체를 생성하는 것은 불가능 합니다.

@Test
void test() {
// AbstractClass is abstract; cannot be instantiated
AbstractClass abstractClass = new AbstractClass();
}

질문 : 추상 클래스는 추상 메서드를 하나라도 가져야 할까?

위 질문에 대한 답은 “NO” 입니다. 추상 클래스는 추상메서드를 가지지 않아도 상관없습니다. 단, “추상 메서드를 하나라도 가지는 클래스는 추상 클래스가 되어야 한다”라고 표현할 수 있습니다. 추상 클래스는 상속(extends)을 통해서만 사용이 가능하며, 하위 클래스의 생성자에서 super()변수를 사용해서 추상 클래스의 생성자를 부르고 초기화 시킵니다.

상속을 이해하는데 가장 중요한 영어 숙어는 is a kind of 입니다. 즉, ~의 한 종류라는 의미를 가지고 있습니다. 예를들어 뽀로로 is a kind of 펭귄 이라하면 뽀로로는 펭귄의 한 종류이다 라는 의미를 가지고 있습니다. 여기서 뽀로로가 하위 클래스가되며 펭귄이 상위 클래스가 됩니다.

다형성(Polymorphism)

인터페이스와 추상클래스를 이해하기 위해서는 다형성(Pholymorphism) 이라는 개념을 익혀야합니다. 또한, 인터페이스와 추상클래스에 대해서 정확히 이해하고 있으면 다형성을 구현할 수 있습니다. 다형성은 여러 객체에게 동일한 명령을 내렸을 때 서로 다르게 반응하는 현상을 의미합니다.

예를들어 동물이라는 상위클래스가 있고, 상위클래스에는 “짖다” 라는 추상메서드를 가지고 있으며 하위 클래스(강아지, 고양이)는 추상메서드를 오버라이딩 하여 강아지 객체에게 명령을 내렸을 때에는 “멍멍”, 고양이 객체에게 명령을 내렸을 때에는 “야옹” 처럼 각자 다른 반응을 나타내는것을 다형성이라고 합니다.

다형성을 구현하는 방법은 크게 4가지가 있으며 상황에 따라 알맞게 선택해서 사용하는 것이 중요합니다.

  • 인터페이스(interface)
  • 추상클래스(abstract)
  • 메서드 오버라이딩
  • 메서드 오버로딩

아래 인터페이스와 추상클래스, 메서드 오버라이딩을 사용한 다형성 구현 예제를 보겠습니다.

동물 추상 클래스는 동물이 가지는 공통 속성인 이름과, 다리에 대한 필드를 가지고 있으며, 동물들의 공통적인 기능인 run 추상 메서드를 가지고 있습니다. 인터페이스는 탈 수 있는 이라는 의미를 가지고 있으며, ridable 이라는 추상 메서드를 가지고 있으며 구현체에서는 추상 메서드를 오버라이딩 하여 사용해야 합니다.

예제를 위에서 배운 be able tois a kind of 를 적용하여 나타내보면 “말 is kind of 동물, 캥거루 is kind of 동물, 말 is able to 탈 수있는” 과 같이 표현 할 수 있습니다.

즉, 추상 클래스와 인터페이스는 be able tois a kind of 이 관계가 성립할 때 사용하는 것이 적절합니다.

지금까지 저희는 인터페이스와 추상 클래스에 대해서 어떻게(how) 사용하는 지와 언제 사용해야 하는지(when) 그리고 왜 사용해야 하는지 조금?(다형성 구현을 위해서, why)에 대해서 배웠습니다. 이제 아래에서는 조금 더 구체적으로 왜 사용해야 하는지(why)와 언제 사용해야 하는지(when)에 대해서 스프링을 기준으로 자세히 배워 보겠습니다.

스프링에서 인터페이스를 사용하는 이유

웹 프로젝트를 진행할때 스프링에서 인터페이스를 정말 많이 사용합니다. 그 중에서도 SI에 정형화된 웹 개발 패턴? 이라 하면 아마 다음과 같은 방식을 많이 사용할 것입니다. repository Service ServiceImpl 인터페이스와 구현체를 만드는 방식을 많이 사용합니다. 위 패턴으로 개발을 하다보면 아시겠지만 보통 인터페이스 하나당 구현체 1개가 매핑되는 형식이 많습니다. 하지만 위에서 배웠듯이 인터페이스를 사용하는 이유 중 하나는 다형성을 구현하는 것이라 했습니다. 그렇다면 “굳이 구현체가 1개인데 인터페이스를 사용해야하나” 라고 의문을 가지 실 수 있습니다. 구현체가 1개임에도 불구하고 사용하는 이유는 확장 가능성decoupling 때문입니다.

확장 가능성에 대한 예제를 보겠습니다. 카드 결제를 담당하는 인터페이스가 있으며, 신한 카드 결제 기능이 필요하며, 앞으로 결제 가능한 카드가 지속적으로 추가될 예정일 경우, 지금 당장 구현체는 1개이지만, 앞으로 추가될 구현체에 대한 확장 가능성 을 열어두는 것입니다.

만약 추가될 가능성이 없다할 경우(이 경우에는 깊은 고민을 해야함) 인터페이스를 두지 않는것이 좋습니다.

다음으로는 인터페이스를 사용하여 의존성을 줄이는 예제(decoupling)를 보겠습니다. 이전에 결합도(coupling)응집도(cohesion)에 대해서 짚고 넘어가자면, 결합도는 소프트웨어 코드의 한 요소가 다른 것들과 얼마나 강력하게 연결되어있는지 얼마나 의존적인지를 의미합니다. decoupling은 결합도를 낮추는것을 의미합니다. 즉, 클래스와 클래스간의 의존 관계를 줄이는 것을 의미하며 결합도를 낮춰서 얻는 이점은 향후 유지/보수를 용이하게 하고 변경에 따른 유연성을 확보하기 위함입니다. 응집도는 프로그램의 한 요소가 해당 기능을 수행하기 위해 얼마만큼의 연관된 책임과 아이디어가 뭉쳐있는지를 나타내는 정도입니다. 따라서 결합도는 낮을수록 좋으며 응집도는 높을수록 좋습니다.

위 유저 조회 메서드 내부에 AService에 대한 객체를 생성한 것을 볼 수 있습니다. AService에 대한 객체가 없으면 해당 메서드는 동작하지 않기 때문에 결합도가 증가합니다. 위 처럼 추상 클래스를 사용하는 경우 생기는 의존성 주입을 아래의 인터페이스 형식으로 바꾸게 되면 의존 관계를 줄일 수 있습니다. 즉, 결합도를 낮출 수 있습니다.

Dependency Injection이란 Component-Scan 방식으로 의존성 주입받는것만 의미하는것이 아니고 위와 같은 코드도 의존성 주입을 의미합니다.

구현체가 아닌 Interface로 의존성 주입

위에서 생성한 Serivce Interface가 있고, 이를 구현한 구현체가 있으며 컨트롤러에서는 Service Interface에 대한 의존성을 주입받아서 사용할 것입니다. 컨트롤러에서 ServiceImple 구현체를 의존성 주입받아서 써도 되지 않을까? 라고 의문을 가지실 수 있습니다. 이에 대한 답은 다음과 같습니다.

인터페이스를 두어서 얻는 이점은 세부 구현체를 숨기고 인터페이스를 바라보게 함으로써 클래스 간의 의존관계를 줄이는 것, 결합도를 낮추고, 다형성을 위해서 입니다. 즉, decouplingpolymorphism 때문입니다.

아래는 @RequiredArgsConstructor 어노테이션의 특성을 활용하여 의존성 주입을 받는 컨트롤러 코드입니다. @RequiredArgsConstructor 어노테이션은 롬복에서 지원하는 어노테이션으로 @NonNull이나 final이 붙은 필드에 대해서 생성자를 생성하는데, 어떠한 빈에 생성자가 오직 한개만 존재하고, 그 생성자의 파라미터 타입이 빈으로 등록 가능한 존재이면 이 빈은 스프링 어노테이션 없이 생성자 주입이 가능한 스프링의 특성을 이용한 것입니다.

하지만, @RequiredArgsConstructor 어노테이션을 사용하여 의존성 주입을 받는 경우에는 전제조건이 하나가 있는데, 바로 Service Inteface의 구현체가 1개여야 한다는 점입니다. 동일한 객체가 여러개 존재 할 경우 스프링 컨테이너(IOC 컨테이너)는 자동 주입 대상 객체를 판단하지 못해서 예외(Exception)를 발생 시킵니다. @Resource 는 객체의 이름을 기반으로 의존성 주입 대상을 찾고, @Autowired는 타입 기반으로 의존성 주입 대상을 찾습니다. @Autowired 어노테이션의 의존 객체를 찾는 순서는 링크로 대체하겠습니다. 해당 링크를 타고 들어가면 자세하게 설명이 되어있습니다.

따라서 구현체가 여러개 일경우 생성자 생성 후 @Qualifier 어노테이션을 사용하여 의존성 주입을 받아야 합니다.

위에서 Service에 대한 의존성을 주입 받는데 어떻게 실제 로직이 구현된 Impl을 동작하게 하지? 라는 궁금증이 생기실 수 있는데, 위에 주석 처리한 코드와 동일한 의미를 가지기 때문입니다.

의존성을 주입받는 방법은 상당히 여러가지가 있습니다. 그 중에서도 가장 추천하는 의존성 주입받는 방법은 생성자를 통한 의존성 주입입니다. 스프링의 철학이 최대한 어노테이션을 걷어내서 스프링에 종속되지 않게 하는것이기 때문입니다.

데이터 전달만 하는 경우에는 repository만 사용

MVC 패턴을 사용하여 개발 할 때, 단순히 데이터 전달만 하고 서비스 계층에서 하는 일이 없는 경우에는 Service Interface와 구현체를 만들지 않아도 됩니다. 굳이 하는일도 없는데 만들 필요는 없기 때문입니다.

결론

인터페이스를 사용하는 이유는 확장 가능성 ,decoupling ,Polymorphism 을 위해서 사용하는 것입니다. 스프링에서 repository와 service, serviceImpl의 정형화된 방식을 사용하여 개발할 때, 인터페이스를 사용하는 이유에 대해서 생각하고, 굳이 인터페이스로 만들어 사용할 때 얻는 이점이 없는 경우에는 클래스로 하는 방법이 더 나을 수 도 있습니다. 따라서 상황에 따라 알맞게 선택해서 사용하는게 중요하다고 생각합니다.

--

--

Dope
Webdev TechBlog

Developer who is trying to become a clean coder.