기타

인터페이스 기반의 개발

wonow_ 2024. 7. 12. 14:04

프로그래밍 패러다임 중 하나인 객체지향 그 중에서 자바 언어를 사용하며 깨닫고 있는 게 있다.

객체지향은 아주 강한 약속이다.

 

객체지향적으로 잘 접근한 코드들은 부연설명 없이 코드만 읽어도 다 이해가 된다.

 

그 중 가장 큰 역할을 하는 게 인터페이스라고 생각한다.

 

인터페이스 기반의 개발은 구현 클래스를 직접 참조하지 않고 상위 인터페이스를 선언하여 다형성을 이용해 프로그래밍 하는 방법이다.

 

인터페이스 기반의 개발을 하지 않으면

 

의존성을 다른 구현체로 바꿨을 때 의존 클래스까지 모두 변경해야한다.

변경에 유연하게 대처하기 어렵고, 유지보수성이 상당히 낮아진다.

 

인터페이스 기반의 개발을 하면

 

구현체의 변경사항이 생겨도 코드의 변화가 적다.

다중 상속이 가능해 인터페이스 개발이 자유롭다.

코드의 재사용성이 높아진다.

구현을 강제할 수 있다. (이거는 이걸 꼭 구현해야돼!!)

인터페이스만 읽어도 클래스 설명이 가능하다.

 

public interface ExceptionCodeProvider {
    HttpStatus getHttpStatus();
    String getMessage();
}

 

내가 작성한 인터페이스다. Exception을 설명해줘야 한다는 인터페이스다.

 

public class MemberException extends RuntimeException implements ExceptionCodeProvider {

    private final ExceptionCode exceptionCode;

    public MemberException(ExceptionCode exceptionCode) {
        super("[Member Exception] : " + exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }

    @Override
    public HttpStatus getHttpStatus() {
        return exceptionCode.getHttpStatus();
    }

    @Override
    public String getMessage() {
        return exceptionCode.getMessage();
    }
}

 

해당 ExceptionCode는 HttpStatus와 String Type Message 를 가지고 있는 Enum이다.

 

ExceptionCodeProvider를 통해서 이거 무조건 구현하라구 했다.

 

왜 이렇게 했을까?

@Getter 를 지양하고, 반복 되는 코드들을 없애려고 했다.

하지만 내가 개발하다보면 편의상 Getter를 쓰고, 까먹을 까봐 인터페이스화로 바꿔 버린 것이다.

기존에는 아래처럼 계속 이렇게 메서드 타고, 타고 들어갔다.

memberException.getExceptionCode().getMessage();

 

 

이건 구현의 강제성을 설명하는 부분이다.

 

Service 레이어로 인터페이스를 설명하는 글은 많으니까 생략

Spring Security의 UserDetails 를 기준으로 설명하겠다

 

    @Override
    @Transactional
    public Long createItem(UserDetails userDetails, ItemCreateDto itemCreateDto, Long brandId) {

        Category category = null;

        Brand brand = brandService.getBrandById(brandId);

        brandService.validateBrand(userDetails, brandId);

        if (itemCreateDto.categoryId() != null) {
            category = categoryService.getCategoryByIdAndBrandId(itemCreateDto.categoryId(),
                    brandId);
        }

        if (itemRepository.checkSameBarcodeInBrand(itemCreateDto.barcode(), brandId)) {
            throw new ItemException(ExceptionCode.SAME_BARCODE_IN_BRAND);
        }

        return itemRepository.save(Item.of(itemCreateDto, category, brand)).getId();
    }

 

해당 코드에서는 UserDetails 인터페이스를 매개변수로 받고 있다.

    @Override
    public void validateBrand(UserDetails userDetails, Long brandId) {

        if (((UserDetailsImpl) userDetails).getRole().equals(JwtUtil.ADMIN)) {

            Brand brand = brandRepository.findByIdWithAdmin(brandId);
            if (!brand.getAdmin().getUsername().equals(userDetails.getUsername())) {
                throw new BrandException(ExceptionCode.YOUR_NOT_ADMIN_THIS_BRAND);
            }
        } else if (((UserDetailsImpl) userDetails).getRole().equals(JwtUtil.MEMBER)) {

            if (!brandRepository.existsByIdAndMemberUsername(brandId, userDetails.getUsername())) {
                throw new BrandException(ExceptionCode.YOUR_NOT_MEMBER_THIS_BRAND);
            }
        }
    }

 

validateBrand다

 

여기에서 UserDetails 를 구현체로 다운캐스팅하여 메서드를 시행한다.

 

@Getter
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {

    private final String username;
    private final String password;
    private final String role;
    private final Collection<? extends GrantedAuthority> authorities;
    private final boolean isAccountNonExpired;
    private final boolean isAccountNonLocked;
    private final boolean isCredentialsNonExpired;
    private final boolean isEnabled;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.isCredentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return this.isEnabled;
    }
}

 

UserDetails 구현체다.

 

여기서는 UserDetails 에는 없는 role 필드가 들어있다.

더 정확히 말하면 getRole() 이라는 추상 메서드가 없었다.

나는 UserDetails 를 더 폭 넓게 사용하기 위해서 role 이라는 필드를 추가했다.

Admin이 로그인 하면 role에 ADMIN 을 넣고 Member 가 로그인하면 role에 MEMBER를 넣는 방식

 

만약에 UserDetailsImpl 말고 다른 구현체를 만든다면, 저 validateBrand() 의 다운캐스팅하는 부분만 수정하면 되지, 서비스 코드를 전부 싹 바꿀 필요가 없다.

 

Generic에서도 인터페이스의 사용은 요긴하게 쓰인다.

 

<? extends Interface>

특정 인터페이스를 상속 받는 클래스만 들어갈 수 있게 제한하는 것이다.

 

그러면 메서드 내에서도 어떤게 들어와도 해당 인터페이스 기반으로만 코드를 짜면 되니, 코드의 변화가 적어진다.

설명도 쉽게 할 수 있다.

 

<? extends flyable>

이면 날 수 있는 애들만 제네릭으로 들어와라

그런 어떤 게 들어오든

?.fly() 를 할 수 있는 것이다.

 

아직 완전히 이해하지 않았지만, 점점 알 수록 재밌어진다....