다형성과 추상화는 매우 중요한 개념이다.
Spring 프레임워크나 Java도 그렇고 이 다형성과 추상화를 기본으로 설계돼 있다.
다형성은 코드를 유연하게 붙여줄 수 있는 놀라운 개념이고
추상화는 그 다형성을 활용하기 위한 인터페이스와 추상클래스를 제공한다.
1개의 클래스는 상속과 확장을 통해 다른 클래스로 변모할 수 있다.
상위클래스인 녀석이 다른 하위클래스 녀석을 참조값으로 사용할 수 있다는 뜻이다.
Memer클래스를 만들고 이 Member를 저장하기 위한 인터페이스를 만든다고 가정하자
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
Optional<Member> findByAge(Integer age);
List<Member> findAll();
}
아래 2. 추상화에서 설명하겠지만 인터페이스는 굳이 다 초기화 하거나 상세하게 설명할 필요는 없다. 대충 이런 메서드를 만들겠다 정도만 가지고 있으면 된다.
이 인터페이스를 확장해서 메모리에 저장하는 구현 클래스를 만들었다고 하자.
public class MemoryMemberRepo implements MemberRepository{
private static Map<Long, Member> memberMap = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(sequence++);
memberMap.put(member.getId(), member);
return member;
}
// 이하 생략
}
안에 들어있는 내용은 별로 중요한게 아님. 하여튼 이런 클래스가 있다손 치자. 그리고 Jpa를 이용해서 Member를 저장하는 구현 클래스도 새로 만들었다.
public class JpaMemberRepo implements MemberRepository{
private final EntityManager em;
public JpaMemberRepo(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
// 이하 생략
}
대충대충하자 어차피 예시니까 하여튼 여러 구현 클래스가 있는 상황이다.
근데 어차피 결국 웹을 실행할 땐 JPA를 쓰든 메모리를 쓰든 하나만 선택해야 한다.
이 때 MemberRepository로 인스턴스를 생성하는데 new JpaMemberRepo()를 박아주면!
걍 JpaMemberRepo로 동작한다.
@Configuration
@AllArgsConstructor
public class SpringConfig {
EntityManager em;
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepo(em);
// return new MemoryMemberRepo();
}
}
Configuration에 MemberRepository를 리턴하는 메서드를 만들었는데 return을 JpaMemberRepo로 해도 먹는다.
심지어 주석처리 해둔 new MemoryMemberRepo도 먹는다. 상황에 따라 원하는 Repository를 취사선택 할 수 있다는거다!
처음에 이거 보고 얼마나 놀랐던지 ㅋㅋㅋㅋ
어? 그거 int나 long 캐스팅 할 때 쓰는거 아닌가?
맞다 그거
근데 이게 부모클래스, 자식클래스의 변환을 말하는거라고도 볼 수 있다.
하위클래스가 상위클래스로 바뀌는건 생략이 가능한데, 그 역은 ()를 꼭 해줘야 한다.
다음 예시를 보자
@Slf4j
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
}
내가 필터 공부할 때 썼던 클래스의 일부다. 여기서 doFilter 메서드는 인자로
ServletRequest, ServletRespose를 받는다.
쟤들은 HttpServletRequest, HttpServletRespose의 상위클래스다.
HttpServletRequest httpRequest = (HttpServletRequest) request;
가 먹는다!!!
request 변수는 ServletRequest지만 그 하위 클래스인 HttpServletRequest로 캐스팅이 되는거다. 이걸 다운캐스팅이라고 한다.
A가 B에 속해 있어용? 을 boolean으로 뱉어준다.
dog instanceof animal // true 이런식임
HttpServletRequest instanceof ServletRequest // true
animal instanceof dog // false
ServletRequest instanceof HttpServletRequest // false
역은 안된다. A가 B의 하위클래스인가용? 이라고 물어보는 놈이라 생각하자
상기 다형성 코드에서 인터페이스라는 놈을 봤다. 코드를 좀 보면, 그냥 선언만 디따 하고 별 내용도 동작코드도 없다. 대충 추상적으로 뭔갈 계획했다 -> 추상화를 했다. 라고 생각하자.
추상화와 관련된 녀석들로는 인터페이스, 추상클래스가 있다.
인터페이스는 구현부가 없다. 변수와 메서드의 선언만 있고
추상 클래스는 최소한 1개의 변수나 메서드가 선언만 있다. (다 구현되어 있으면 걍 클래스지 뭐)
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
Optional<Member> findByAge(Integer age);
List<Member> findAll();
}
얘가 인터페이스다. 앞에 인터페이스라고 떡하니 붙어 있쥬?
저렇게 구현이 하나도 없이 변수나 메서드만 때려 박은 것을 인터페이스라고 한다.
물론 구현된 변수는 있을 수 있는데 그녀석은 public static final을 붙여서 상수로 만들어줘야 한다. fianl 안붙여도 내려오면 상수됨.
애초에 확장을 위해 만들어진 녀석임.
기본 기획을 인터페이스로 다 해두고 이를 하나하나 구현해야 하겠다.
이 때 저 인터페이스를 상속받으려면 implements 라는 단어를 쓰면 된다.
public class MemoryMemberRepo implements MemberRepository{
private static Map<Long, Member> memberMap = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(sequence++);
memberMap.put(member.getId(), member);
return member;
}
// 이하 생략
}
MemberRepository를 상속 받아서 MemoryMemberRepo를 만들었다. 여기서 중요한 점은 인터페이스를 상속받아 클래스를 만들면, 추상적으로 만든 메서드들을 죄~다 구현해줘야 한다. (오버라이딩)
implements는 여러개를 받을 수 있다. 다양한 기본 인터페이스를 만들고 1개의 구현체를 만드는 방법도 있다. 그리고 그게 OOP에는 더 알맞는 방법임
추상클래스는 abstract를 붙이고 클래스를 만들면 된다.
abstract class Animal {
private String name;
Animal(){}
Animal(String name){
this.name = name;
}
public String getName(){return name;}
void setName(String name){this.name = name;}
abstract void cry();
void myName(){System.out.println("이름은 " + getName());}
}
// void cry()는 추상 메서드임. 하위클래스에서 재정의해서 강제로 사용해야함.
// void myName()은 강제성이 없음. 일반 메서드랑 같다.
// 그래서 하위클래스에서 그냥 사용이 가능함.
상기 코드는 작년에 공부할 때 테스트로 내가 작성한 코드임
abstract 클래스를 직접 만들었는데, 인터페이스와 확연히 다른 점은 맨 아래 void myName()이 구현되어 있다는 점이다.
인터페이스는 구현된 메서드가 없는데 여긴 있을 수 있음. 이걸 상속 받아서 클래스를 만들면 abstract void cry()는 꼭 구현을 해줘야 한다.
추상 클래스는 인터페이스와 다르게 extends로 받아온다.
클래스를 만들 때, 여러 인터페이스를 확장할 수 있는 implements와 다르게 extends는 단 1개만 가능
class Student extends Person implements Allowance, Train{
Student(String name, int age, int weight){
super(name, age, weight);
}
public void in(int price, String name){
System.out.println(price + "원 을" + name + "에게 받았습니다.");
}
public void out(int price, String name){
System.out.println(price + "원 을을" + name + "에 썼습니다.");
}
public void train(int price, String name){
System.out.printf("%s(을)를 %d원에 공부했다.", name, price);
}
}
두둥 이것도 혼자 공부할 때 만든 클래스 ㅋㅋㅋ
Student 클래스는 추상클래스 Person을 받고, 인터페이스 Allowance, Train을 받는다.
개인적으로 객체지향 프로그래밍의 가장 핵심이 이 추상화와 다형성인거 같다. 이 덕에 미칠듯한 확장성과 유지보수에 매우 유리하다.
인터페이스를 만들고 어떤 클래스를 개발을 했다. 근데 정책이 바뀌어서 클래스 전체를 바꾸기보단, 인터페이스로 또 다른 클래스를 새로 개발을 한 뒤에 바꿔서 껴주면 되니까! (다형성 덕에 쌉가능)
물론 설계할 때 타입을 인터페이스로 지정해두고 하긴 해야겠지만 ㅎㅎ