[Clean Code] 클린코드-3

Junho Bae·2021년 3월 15일
0

Clean Code

목록 보기
2/3

clean code-3

“클린코드 - 애자일 소프트웨어 장인정신”, 로버트 C.마틴 님의 책을 읽고 정리한 내용입니다.

chapter 10. 클래스

지금까지 코드 행, 코드 블록, 함수의 구현, 함수의 관련을 맺는 법 등을 살펴보았고 이제부터는 더 고차원적인 클래스를 잘 작성하는 법을 살펴보려고 합니다.

클래스의 체계

클래스의 체계는 다음과 같습니다.
1) static public 상수
2) static private 변수
3) private 인스턴스 변수

4) 공개 함수
5) 자신을 호출하는 공개 함수 직후에 비공개 함수.

이처럼 추상화 단계가 순차적으로 내려가야 합니다.
-캡슐화
: 변수와 함수는 공개하지 않아도 되지만 꼭 숨길 필요는 없습니다. 테스트 코드에 접근을 허용해야 하기 때문에, protected 선언을 하거나 혹은 패키지 전체로 공개를 하기도 합니다.
하지만 이렇게 캡슐화를 푸는 것은 언제나 최후의 수단이 되어야 합니다.

클래스는 작아야 한다.

public class SuperDashboard extends JFrame implements MetaDataUser{
	
	public Component getLastFocusedComponent()
	public void setLastFocusedI(Component lastFocused)
	public int getMajorVersionNumber()
	public int getMinorVersionNumber()
	public int getBuilderName()

	}

클래스를 만들 때는 무조건 작아야 합니다. 그러면 얼마나 작아야 할까요?
-앞서 함수는 행 수로 크기를 측정했지만, 클래스는 책임을 세어야 합니다.
-클래스 메서드의 숫자가 적더라도, 책임이 적은 것은 다른 이야기로 봐야 합니다.
-작명이 클래스의 크기를 줄이는 첫 관문입니다. 작명을 통해서 클래스의 책임을 기술해야 하기 떄문입니다.
-클래스의 간결한 이름이 떠오르지 않는다? = 클래스의 크기가 너무 크다,
-Processor, Manager, Super 등과 같이 모호한 이름은 클래스의 크기가 크다는 얘기입니다!
-클래스는 “if”, “and”, “or”, “but” 을 사용하지 않고 25단어 내외로 설명이 가능해야 합니다.
ex) “SuperDashboard”는 마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며 버전과 빌드 번호를 추적하는 메커니즘을 제공한다.
: “제공하며” 라고 넘어가는 순간 책임이 많다는 뜻!

단일 책임 원칙 SRP(Single Responsibility Principle)

클래스나 모듈을 변경할 이유가 하나, 단 하나 뿐 이어야 한다.

위의 SuperDashboard는 작아보이지만 변경할 이유가 두가지 입니다.
1) 위 클래스는 소프트웨어 버전 정보를 추적하는데, 버전 정보는 소프트웨어를 출시할 떄마다 달라집니다.
2) 위 클래스는 스윙 컴포넌트를 관리하는데, 스윙 코드를 변경할 때마다 버전 번호가 달라집니다.
-> 변경할 이유를 파악하려 애쓰다 보면, 코드를 추상화하기도 쉬워집니다.
-> 가령, 위의 클래스에서 버전을 다루는 메서드 세 개를 따로 빼내서 Version이라는 클래스를 만든다면?

소프트웨어를 돌아가게 만드는 활동과 소프트웨어를 깨끗하게 만드는 활동은 완전히 별개다. 우리들 대다수는 두뇌 용량에 한계가 있어 ‘깨끗하고 체계적인 소프트웨어’ 보다는 ‘돌아가는 소프트웨어’에 초점을 맞춘다. 전적으로 올바른 태도다.

문제는 우리들 대다수가 프로그램이 돌아가면 일이 끝났다고 여기는 데 있다. 깨끗하고 체계적인 소프트웨어 라는 다음 관심사로 전환하지 않는다. 프로그램으로 되롤아가 만능 클래스를 단일 책임 클래스 여럿으로 분리하는 대신 다음 문제로 넘어가버린다.

응집도 Cohesion

클래스는 인스턴스 변수 수가 작아야 합니다.

각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 합니다.

즉, 일반적으로 메서드가 변수를 더 많이 사용할 수록 메서드와 클래스는 응집도가 더 높습니다. 그렇다면 모든 메서드가 모든 인스턴스 변수를 다 사용하는 것이 응집도가 높다는 뜻이겠죠?

하지만, 이는 가능하지도 바람직하지도 않습니다. 다만, 우리는 높은 응집도 클래스를 선호합니다. 클래스의 변수와 메서드가 서로 의존적이며 논리적인 단위로 묶인다는 뜻이기 때문입니다.

”함수를 작게, 매개변수 목록을 짧게” 라는 전략을 따르다 보면 몇몇 메서드만이 시용하는 인스턴스 변수가 아주 많아집니다. => 쪼개야 합니다!

응집도를 유지하면 작은 클래스 여럿이 나온다.

가령, 큰 함수를 여럿으로 나누는 경우.

큰 함수의 일부를 작은 함수로 빼내자! -> 그런데 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다! -> 새 함수의 인수로 넘기지 말고 인스턴스 변수로 빼내자! -> 몇몇 함수만 쓰는 인스턴스 변수구나, 클래스로 분리하자.

와 같은 방법으로, 큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생깁니다.

변경하기 쉬운 클래스

대부분의 시스템은 지속적으로 변경이 가해집니다. 그리고 변경할 때 마다 의도대로 동작하지 않을 위험이 따릅니다. 따라서, 깨끗한 시스템을 설계하여 클래스를 체계적으로 정리하여야 합니다.

수정 사항이 생길 때, 코드에 “손대면” 위험이 생길 수 있습니다. 어떤 변경이든 간에, 다른 코드를 망가뜨릴 위험이 존재한다는 것입니다. 그렇다면 테스트도 완전히 다시 짜야 될겁니다.

따라서, 책의 SQL 예제와 같이 파생 클래스로 쪼개고, 특정 메서드에서만 사용되는 비공개 메서드는 해당하는 파생 클래스 등으로 옮기는 등의 방식이 필요합니다.

그렇다면, SRP 뿐만 아니라 OCP의 원칙도 지킬 수 있습니다.

OCP란 확장에 개방적이고, 수정에 폐쇄적이어야 한다는 원칙인데, 파생 클래스를 생성하는 방식으로 새 기능에 개방적인 동시에 다른 클래스를 닫아 놓는 방식으로 수정에 폐쇄적으로 만들 수 있습니다. 새 기능을 그냥 끼워 넣기만 하면 되는 겁니다.

변경으로부터 격리

요구사항은 변경되기 때문에, 상세 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠집니다. 따라서, 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리해야 합니다.

가령, Portfolio 클래스를 만드는 경우, 이 클래스는 외부 TokyoStockExchange API를 사용해 포트폴리오의 값을 계산합니다. 이 때 우리의 테스트 코드는 시세 변화에 영향을 받을 수 밖에 없습니다. 값이 계속해서 바뀌기 때문에 이런 API로 테스트 코드를 짤 수는 없겠죠?

우선, 이런 인터페이스를 선언 합니다.

public interface StockExchange {
	Money curretnPrice(String symbol);
}

그리고, 이런 인터페이스를 구현하는 TokyoStockExchange 같은 클래스를 구현합니다.


public class TokyoStockExchange implements StockExchange {
	//해당 api (도쿄의 api)를 받아와 처리 하는 클래스
}

그렇다면 Portfolio 클래스는 다음과 같이 구현이 가능합니다.
생성자에서는 excahnge 참조자를 인수로 받아서 어떠한 exchange를 사용할 지 정할 수 있습니다.


public Portfolio {
	private StockExchange exchange;
	public Portfolio(StockExchange exchange) {
		this.exchange = exchange;
	}
}

그렇다면 테스트 코드 작성은, TokyoStockExhange를 흉내내는 테스트 클래스를 만들 수 있습니다. 테스트용 클래스는 StockExchange를 구현하며 고정된 주가를 반환하는 걸로 만들어버리는 거죠.


public class PorfolioTest{
	private FixedStockExchangeStub exchange;
	private Portfolio portfolio;

	@Before
	protected void setUp() throws Exception {
		exchange = new FixedStockExchangeStub();
		exchange.fix("MSFT",100);
		portfolio = new Portfolio(exchange);
	}

	@Test
	public void GivenFiveMSFTTotalShouldBe500() throws Exception {
		portfolio.add(5,"MSFT");
		Assert.assertEquals(500,portfolio.value());
	}

}

이와 같이, 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용 서이 더욱 높아집니다.

결합도를 낮춘다 = 시스템 요소가 다른 요소로부터, 그리고 변경으로부터 잘 격리되어 있다.

=> 그러다 보면, DIP 를 지키는 원칙 역시 나옵니다. 추상화에 의존해야 한다는 것이죠.

chapter 11. 시스템

복잡성은 죽음이다. 개발자에게 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.” - 레이 오지, 마이크로소프트 CTO

이 장에서는 마치 도시를 설계하듯, 높은 추상화 수준, 즉 시스템 수준에서 깨끗함을 유지하는 방법을 살펴봅니다. 약간 읽다 보니까 Spring의 원리에 적용되는 부분도 많았던 것 같습니다.

시스템 제작과 시스템 사용을 분리하라.

제작사용과 아주 다르다는 사실을 명심해야 합니다. 즉, 준비 과정과 런타임 로직을 분리해야 한다는 것입니다.

가장 먼저 살펴보아야 하는 것은 관심사, 즉 시작 단계입니다. 많은 어플리케이션들이 시작단계인 관심사를 분리하지 않습니다.

public Service getService() {

	if (service == null)
		service = new MyServiceImpl(...);
	return service;
}

이는 초기화 지연, 혹은 계산 지연이라는 기법인데, 실제로 객체가 필요할 때 까지 생성하지 않으므로 부하가 없기 때문에 시작 시간이 빨라지고, 어떠한 경우에도 null 포인터를 반환하지 않습니다.

하지만, 현재 getService 메서드는 MyServiceImpl() 생성자 인수에 명시적으로 의존합니다. 따라서, 런타임 로직에서 객체를 전혀 사용하지 않더라도 의존성을 해결하지 않으면 컴파일 자체가 안됩니다!

테스트도 문제고, 일반 런타임 로직에 생성 로직도 섞여있다 보니 모든 실행 경로도 테스트 해야 하고, 저 객체가 모든 상황에 적합한 객체인지도 모릅니다.

물론 이런 초기화 지연 기법을 한 번 정도 사용한다면 큰 문제는 아니지만, 이처럼 “좀스러운 설정 기법”을 수시로 사용한다면 모듈성이 저조하고 중복이 심각해집니다! 이러한 손쉬운 기법으로 모듈성을 깨서는 절대 안됩니다.

Main 분리

: 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생서오디었고 모든 의존성이 연결되어있다고 가정.

즉, main 함수에서 시스템에 필요한 객체를 생성한 후 이를 어플리케이션에 넘기면 되는 거고, 어플리케이션은 사용만 하면 됩니다!

팩토리

당연히, 언제 어떤 객체가 필요한지 어플리케이션이 결정할 필요도 있죠. 주문 처리 시스템에서는 아이템 인스턴스를 생성해 오더에 추가할 수 있죠.

이 때는 ABSTRACT FACTORY 패턴을 사용합니다. 그러면 아이템을 생성하는 시점은 어플리케이션이 결정하지만, 그 코드는 어플리케이션이 모릅니다.

의존성 주입

사용과 제작을 분리하는 강력한 메커니즘 하나가 바로 그 유명한 의존성 주입, Dependency Injection입니다.

DI는 IoC(제어 역전) 기법을 의존성 관리에 적용한 메커니즘으로, 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘깁니다. 상위 모듈만을 의존하기 때문에, 새로운 책임은 새로운 구현체를 만들어서 넘기면 되니까 그런 것 같습니다. 즉, 단일 책임 원칙 을 지키게 되는 겁니다.

여기서 객체는 의존성 자체를 인스턴스로 만드는 책임을 지지 않고, 이러한 책임을 전담으로 하는 메커니즘이 있어야 하고, 그렇게 함으로써 제어를 역전 합니다.

진정한 의존성 주입은 여기서 더 나아가, 클래스가 의존성을 해결하려 시도하지 않고, 클래스는 완전히 수동적으로, 의존성을 주입하는 설정자 메서드나 생성자 인수를 제공할 뿐입니다.

DI 컨테이너는 요청이 들어올 때 마다 필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성을 설정해줍니다.

스프링이 가장 널리 알려진 자바 DI 컨테이너를 제공하죠. IoC 컨테이너… 그리고 객체 사이의 의존성은 xml 파일에 저장을 해주죠.

DI를 사용하더라도 초기화 지연은 여전히 사용됩니다.

확장

’처음부터 올바르게’ 시스템을 만들 수 있다는 믿음은 미신이다. 대신에 우리는 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다., 내일은 새로운 스토리에 맞춰 시스템을 조정하고 확장하면 된다.

TDD, 리펙터링, 이를 통해 얻어지는 깨끗한 코드 수준에서는 시스템 조정과 확장이 쉽습니다. 근데 시스템 자체도 그렇게 가능할까요? 단순한 아키텍처를 복잡한 아키텍처로 키울 수 있을까요?

소프트웨어 시스템은 “수명이 짧다”는 본질로 인해, 적절한 관심사의 분리를 통해 아키텍처의 점진적인 발전이 가능합니다.

본문에서는 EJB1과 EJB2 아키텍처를 소개합니다. 이들은 관심사를 적절히 분리하지 못해 유기적인 성장이 어려웠습니다.

EJB는 Enterprise Java Bean으로, 기업 환경의 시스템을 구현하기 위한 서버 측 컴포넌트 모델입니다. 웹에서는 JSP로 화면 로직을 처리하고, EJB로는 업무 로직을 처리한다나….

Bank 클래스에 필요한 엔티티 빈은, 비즈니스 논리와 어플리케이션 컨테이너가 강하게 결합되어, 컨테이너가 비즈니스 논리가 매우 크고 밀접합니다. 그러다 보면 컨테이너를 흉내 내거나 많은 시간 낭비를 하며 EJB와 테스트를 실제 서버에 배치해야 합니다. 그러다 보니, 프레임 워크 밖에서 재사용한다? 불가능합니다.

횡단 관심사 cross-cutting

사실 ejb를 잘 몰라서 정확하게 이해하기는 조금 힘들었습니다 ㅋㅋ…

무튼,EJB2 아키텍처는 관심사를 거의 완벽하게 분리한다고 합니다. 일부 영속적인 동작은 소스코드가 아니라 배치 기술자에서 정의하는거죠.

영속성과 같은 관심사는 어플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있습니다. 모든 객체가 전반적으로 동일한 방식을 이용하게 만들어여 합니다.

그렇다면, 모듈화와 캡슐화로 영속성 방식을 생각할 수 잇지만, 현실적으로 그렇다면 영속성 방식을 구현한 코드가 온갖 객체로 흩어진답니다. 여기서 횡단 관심사라는 용어가 나온다고 합니다.

EJB 아키텍처가 보안, 영속성, 트랜잭션을 처리하는 방식은 AOP를 예견했다고 본답니다. 호호…

AOP에서 Aspect란 “특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다”고 명시합니다.

영속성을 예로 들면, 프로그래머는 영속적으로 저장할 객체와 속성을 선언한 후 영속성 책임을 영속성 프레임워크에 위임한다. 그러면 aop 프레임 워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다.

자바에서 사용한 관점, 혹은 관점과 유사한 메커니즘1 - 자바 프록시

자바 프록시는 단순한 상황에 적합합니다. 책의 예제를 살펴보면 jdk 프록시를 사용해 영속성을 지원하는 예제를 볼 수 있습니다.

여기서는 프록시로 감쌀 인터페이스인 bank와 비즈니스 논리를 구현한 POJOBankImpl을 정의하였습니다.

프록시 API에는 InvocationHandler를 넘겨줘야 하고, 넘긴 InvocationHandler는 프록시에 호출되는 Bank메서드를 구현하는데 사용됩니다.

복잡합니다! 양도 많고. 이 양과 크기는 프록시의 단점입니다. 즉, 프록시를 쓰면 깨끗한 코드를 쓰기가 어렵죠.

순수 자바 AOP 프레임 워크

다행히, 프록시 코드는 대부분 판박이라 도구로 자동화가 가능합니다. 다양한 순수 자바 관점의 프레임워크들도 내부적으로는 프록시를 사용합니다. 특히, 스프링은 비즈니스 논리를 POJO로 구현한다고 합니다.

POJO는 순수하게 도메인에 초점을 맞추고, 엔터프라이즈 프레임워크에 의존하지 않기 때문에 테스트가 개념적으로 더 쉽고 간단하며, 유지 보수에도 용의합니다.

프로그래머는 설정 파일이나 api를 이용해 필수 구조를 구현하는데 여기에는 영속성, 트랜잭션,보안, 캐시 장애조치 등이 포함됩니다. 그리고 프레임워크는 이를 프록시나 바이트코드 라이브러리를 사용해 구현합니다! 즉, DI 컨테이너의 구체적인 동작을 제어하는 거죠. app.xml에서 빈을 설정하는 등입니다.

Bank 도메인 객체를 빈으로 뜯어보면, 사실상 Bank 도메인 객체는 DAO로 프록시 되어 있으며, DAO는 JDBC 드라이버 자료 소스*로 프록시 되어 있습니다.*

=> 그래서 “마뜨료쉬까”라고 하는 것 같습니다.

그러다보니, 클라리언트는 bank 객체에서 getAccounts()를 호출한다고 믿고 있으나 실제론로는 이러한 데코레이터 객체 집합의 가장 외곽과 통신하는 것입니다. 물론 필요하다면 트랜잭션, 캐싱 등도 데코레이터를 추가할 수 있겠져.

만약 어플리케이션에서 DI컨테이너에게 시스템 내 최상위 객체를 요청하는 코드는 다음과 같습니다.

XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("app.xml", getClass());

Bank bank = (Bank) bf.getBean("bank:);

이는 스프링 관련 자바 코드가 거의 필요없으므로, 어플리케이션은 사실상 스프링과 독립적입니다. 즉, EJB2 시스템이 지니고 있던 강한 결합 문제가 사라졌죠.

이러한 xml 방식의 설정은 장황하고 읽기 어려울지라도 정책이 겉으로 보이기 때문에 프록시, 관점 논리보다 단순해 xml과 어노테이션을 EJB3부터는 따르게 되었습니다. 스프링 모델을 따랐다고 하네요.

예제처럼 기존의 코드를 EJB3로 작성하니 훨씬 보기 좋습니다. 스프링에서 많이 보던 코드네요. 상세 정보들이 어노테이션으로 그대로 들어가, 그대로 남아 있으면서도 깔끔해졌기 때문이죠. 따라서 코드 테스트와 개선이 훨씬 수월합니다.

AspectJ 관점

관심사를 관점으로 분리하는 가장 강력한 도구는 AspectJ 언어입니다. 강력하고 풍부하지만 새 문법과 언어를 익혀야 합니다. 그냥 그렇다고 하고 넘어갑니다.

테스트 주도 시스템 아키텍처 구축

아주 단순하면서도 멋지게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 결과물을 재빨리 출시한 후, 기반 구조를 추가하며 조금씩 확장해도 괜찮다는 말이다.

관점 혹은 그와 유사한 개념으로 관심사를 분리하여, 어플리케이션 도메인 논리를 POJO로 작성할 수 있다면, 즉 코드 수준에서 아키텍처 관심사를 분리할 수 있다면* 진정한 테스트 주도 아키텍처 구축이 가능하다고 하네요.*

결국 결론은

최선의 시스템 구조는 각기 POJO 객체로 구현되는 모듈화된 관심사 영역(도메인)으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합된다. 이러한 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.

의사 결정을 최적화 하라.

모듈을 나누고 관심사를 분리하면 지엽적인 관리와 설정이 가능해집니다. 즉, 가장 적합한 사람에게 책임을 맡기면 가장 좋은거죠.

우리는 때때로 가능한 마지막 순간까지 결정을 미루는 방법이 최선이라는 사실을 까먹곤 한다. 게으르거나 무책임해서가 아닌, 최대한 정보를 모아 최선의 결정을 내리기 위해서다.

관심사를 모듈로 분류한 POJO 시스템은 기민하기 때문에 최신 정보 기반 최선 시점에 빠르게 결정을 내릴 수 있습니다.

창발성

단순한 설계 규칙 네 가지가 소프트웨어의 품질을 크게 높여준다고 합니다.

1.모든 테스트를 실행한다.*
2.중복을 없엔다*
3.프로그래머 의도를 표현한다.*
4.클래스와 메서드 수를 최소로 줄인다.*
중요도 순

1. 모든 테스트를 실행하라

-설계는 의도한 대로 돌아가는 시스템을 만들어야 합니다. 문서만 완벽해봤자, 검증할 방법이 없으면 의미가 없습니다.

테스트를 철저히 거져 모든 테스트 케이스를 항상 통과하는 시스템은 ‘테스트가 가능한 시스템’이다. 당연하지만 중요한 말이다. 테스트가 불가능한 시스템은 검증도 불가능하다. 논란의 여지는 있지만, 검증이 불가능한 시스템은 절대 출시하면 안된다.
-테스트가 가능한 시스템을 만들려고 하다보면 크기가 작고 목적 하나만 수행하는 클래스가 나오고, SRP를 준수하는 클래스는 테스트가 훨씬 쉽습니다.
-결합도가 높은 코드는 테스트 케이스 작성이 어렵기 때문에, DIP, 즉, 의존성 주입, 인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮춰야 합니다.

리팩터링

2번부터 4번은 리팩터링의 영역입니다. 일단 테스트케이스를 모두 작성하고, 코드와 클래스를 정리합니다. 즉, 코드와 클래스를 점진적으로 리팩터링 하는거죠. 코드를 쓰고 나서는 설계를 다시한번 봐야 합니다. 코드를 정리하면서 시스템이 깨질까 걱정할 필요는 없습니다. 테스트 케이스가 있으니까요!
-설계 품질을 높이는 기법이라면 무엇이든 적용해도 괜찮습니다. 응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 등등의 모든 방법을 동원합니다.

2. 중복을 없애라

중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻합니다.
-똑같은 코드
-구현중복

int size(){}
boolean isEmpty(){}

//isEmpty를 사용할 때 size를 사용하자
boolean isEmpty(){
	return 0 == size();
}

isEmpty를 굳이 다른 방법으로 구현할 필요 없이, size()를 활용.

깔끔한 시스템을 만들려면 단 몇줄이라도 중복을 제거하겠다는 의지가 필요합니다.
-> 예제에서 볼 수 있듯이, 중복되는 코드를 새로운 메서드로 뽑아내면 클래스가 SRP를 위반하는지를 포착할 수 있습니다.
-> 그렇다면 이를 새로운 클래스로 옮겨 가독성을 더욱 올리고
-> 팀원들에게 이러한 메서드를 더 추상화해 다른 맥락에서 재사용할 기회를 줄 수도 있습니다.

Template Method 패턴


public class VacationPolicy {
    public void accrueUSDivisionVacation() {
        //지금까지 근무한 시간을 바탕으로 휴가일수를 계산하는 코드
        //...
        //휴가 일수가 미국 최소 법정 일수를 만족하는지 확인하는 코드
        //...
        //휴가 일수를 급여 대장에 적용하는 코드
        //...
    }
    
    public void accureEDUivisionVacation() {
        //지금까지 근무한 시간을 바탕으로 휴가일수를 계산하는 코드
        //...
        //휴가 일수가 유럽연합 최소 법정 일수를 만족하는지 확인하는 코드
        //...
        //휴가 일수를 급여 대장에 적용하는 코드
        //...
    }
}

이 코드는 법정 일수를 계산하는 코드만 빼면 거의 동일한 메서드죠. 직원 유형에 따라 살짝 바뀐다거나. 이를 Template Method 패턴을 적용해 중복을 제거합니다.

package study.datajpa.entity;

abstract public class VacationPolicy {
    
    public void accureVacation() {
        calculateBaseVacationHours();
        alterForLegalMinimums();
        applyToPayroll();
    }
    
    private void calculateBaseVacationHours(){/**/};
    abstract protected void alterForLegalMinimums{/**/};
    private void applyToPayroll{/**/};
}


public class USVacationPolicy extends VacationPolicy {
    @Override
    protected void alterForLegalMinimums() {
        //미국 최소 버법정 일수 사용
    }
}

public class EUVacationPolicy extends VacationPolicy {
    @Override
    protected void alterForLegalMinimums() {
        //유럽 최소 버법정 일수 사용
    }
}

이처럼 abstract로 만든걸 상속시켜서, 중복되지 않는 정보만 제공하고 구현체에서는 구멍을 매꾸면 된다.

뒤의 내용은 지금까지 언급한 내용을 정리합니다. 유지 보수 개발자가 초기 작성자보다 문제를 더 잘 이해할리가 없기 때문에, 작성하는 사람은 자신이 언제든 유지보수 개발자가 될 수 있다는 생각을 하고 주의깊게 잘 작성해줘야 한다는 겁니다. 그 구체적인 방법들은

좋은 이름 선택, 함수와 클래스 크기 줄이기, 표준 명칭 사용하기, 단위 테스트 꼼꼼하게 작성하기, 그리고 노력하기 입니다.

chapter 13. 동시성

객체는 처리의 추상화다. 스레드는 일정의 추상화다. - 제임스 O.코플리엔
동시성, 스레드와 관련된 장입니다. 동시성과 깔끔한 코드는 양립하기 아주아주 어렵다고 합니다. 스레드를 하나만 돌리는 코드는 쉽고, 겉으로는 멀쩡한데 깊숙한 곳에 있는 코드도 짜기 쉽습니다. 물론 부하를 받기 전 까지는 잘 돌아가겠지만요.

사실 동시성 자체는 매우 어려운 주제 이기 때문에, 본문에서는 후에 다시 다룹니다.

동시성이 필요한 이유?

동시성은 결합을 없애는 전략입니다. “무엇” 과 “언제”를 분리하는 전략이라는 겁니다.
그 이유는, 어플리케이션 구조와 효율이 극적으로 나아지기 때문입니다. 구조적인 관점에서 프로그램을 작은 여러 협력 프로그램으로 보이기 때문에, 시스템을 이해하기가 쉽고 분리하기도 쉽습니다.

서블릿 모델
대표적으로, 웹 어플리케이션이 표준으로 사용하는 서블릿 모델이 있습니다.
-서블릿은 웹 or EJB 컨테이너라는 우산 아래에서 돌아가는데, 이들 컨테이너는 동시성을 부분적으로 관리합니다.
-웹 요청이 들어올 때마다 웹 서버는 비동기식으로 서블릿을 실행합니다.
-원칙적으로 각 서블릿 스레드는 다른 서블릿 스레드와 무관하게 자신만의 세상에서 돌아갑니다.
-이러한 웹 컨테이너가 제공하는 결합분리 전략은 완벽과 거리가 멀기 때문에, 프로그래머는 동시성을 정확하게 구현하도록 각별한 주의와 노력을 기울여야 합니다.*

장점
단순 구조 개선 뿐만이 아니라 여러 이점이 있습니다.
-수 많은 웹 사이트에서 정보를 가져와 요약하는 수집기 : 한 스레드만 돌린다면 한 페이지만 한번씩 방문? 요구사항으로 인해 동시성 구현이 필수적
-한번에 한 사용자를 처리하는 시스템? no..
-정보를 나눠 여러 컴퓨터에 돌리면 어떨까, 대량의 정보를 병렬로 처리한다면 어떨까?

미신과 오해

동시성은 항상 성능을 높여준다?

  • 동시성은 “때로” 성능을 높여주나, 대기 시간이 아아아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분이 많은 경우에만 성능이 높아지는데 이는 일상적인 상황이 아닙니다.
    동시성을 구현해도 설계는 변하지 않는다.
  • 단일 스레드와 다중 스레드는 설계가 매우매우 다릅니다. 무엇과 언제를 분리한다면 구조가 크게 달라집니다.
    웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다. 내 얘긴가?**
    -실제로 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지 알아야 합니다.

동시성은 다소 부하를 유발한다.
-성능 측면에서 부하가 오며, 코드도 더 짜야 합니다.
동시성은 복잡하다.
-맞습니다. 간단한 문제도 복잡해집니다.
일반적으로 동시성 버그는 재현하기 어렵다.
-그래서 일회성 문제로 여겨 무시됩니다.
동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

난관

public class X{
	private int lastIdUsed;
	
	public int getNextId(){
		return ++lastIdUsed;
	}
}

인스턴스 X를 생성하고, lastIdUsed를 42로 설정한 다음, 두 스레드가 이를 공유한다. 두 세르드가 getNextId()를 호출한다면

1) 한 스레드는 43을 받는다. 다른 스레드는 44를 받는다. lastIdUsed는 44가 된다.
2)한 스레드는 44를 받는다. 다른 스레드는 43을 받는다. lastIdUsed는 44가 된다.
3)한 스레드는 43을 받는다. 다른 스레드는 43을 받는다. lastIdUsed는 43을 받는다.

3번은 왤까? 바이트 코드만 고려한다면, 두 스레드가 메서드를 실행하는 잠재적인 경로는 최대 12,870개에 달하며, 만약 이 변수가 long 이면 2,704,156개가 되는데, 이 중 일부경로가 잘못된 대답을 내놓기 때문입니다.

동시성 방어 원칙

1) 단일 책임 원칙
동시성 관련 코드는 다른 코드와 분리해야 합니다. 즉, 책임을 하나로 져야 한다는 겁니다.
-동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
-동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.
-잘못 구현한 동시성 코드는 별의별 방식으로 실패한다! 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.

2) 따름 정리 : 자료 범위를 제한하라
위의 난관의 예시처럼, 예상치 못한 결과가 나올 수 있습니다. 이러한 문제를 해결하기 위해서는, 공유 객체를 사용하는 코드 내 임계영역을 synchronized 키워드로 보호하라고 권장합니다. 이걸 줄여야 합니다. 이걸 안줄이면
-보호할 임계영역이 빠지고 코드가 망가진다.
-모든 임계영역을 올바로 보호했는지 확인하느라 개고생한다.
-그렇지 않아도 찾아내기 어려운 버그가 더 어려워진다.

자료를 캡슐화 하고, 공유 자료를 최대한 줄이자.

3) 따름 정리 : 자료 사본을 사용하라
공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 좋습니다. 읽기 전용으로 객체를 복사하던가, 다 쓰고 나서는 사본을 쓴다던가.

복사 비용 즉 사본 생성과 가비키 컬랙션에 드는 부하비용은, 사본으로 동기화를 피하는 경우 내부 잠금을 없애며 절약한 수행 시간과 상쇄될 가능성이 큽니다.

4) 따름 정리 : 스레드는 가능한 독립적으로 구현하라.
자신만의 세상에 존재하는 스레드를 구현합니다. 즉, 다른 스레드와 자료를 공유하지 않는거죠.
-각 스레드는 클라이언트 요청 하나를 처리하고, 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장합니다. 그러면, 다른 스레드와 동기화할 필요가 없죠.
-가령 HttpServlet 클래스의 파생 클래스는 도든 정보를 doGet, doPost를 매개변수로 받아 각 서블릿은 마치 자신이 독자적인 시스템에서 동작하는 양 요청을 처리합니다. 물론 서블릿을 쓴다면 데이터베이스를 공유하니까 문제가 생기죠.

독자적인 스레드로, 가능하다면 다른 프로세서에서 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.

라이브러리를 이해하라.

현재는 자바8이지만… 책에 따르면 자바 5에서 동시성 환경을 구현한다면 특히 참고해야할 프레임 워크가 있습니다.
-스레드 환경에 안전한 컬렉션을 사용
-서로 무관한 작업을 수행할 때는 executor 프레임 워크 사용
-가능하다면 스레드가 차단되지 않는 방법을 사용
-일부 클래스 라이브러리는 스레드에 안전하지 못하다,

스레드 환경에 안전한 컬렉션
-java.util.concurrent 패키지의 라이브러리
-ConcurrentHashMap: 다중 스레드 환경에서 안전하고 성능도 좋으며, 동시 읽기 쓰기 지원.
-ReentrantLock, Semaphore, CountDownLatch 등등..

실행 모델을 이해하라

용어정리
1. 한정된 자원 : 다중 스레드 환경에서 사용하는 자원. 크기나 숫자가 제한적
2. 상호 배제 : 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우
3. 기아(Starvation) : 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다리는 경우. 가령, 맨날 짧은 쓰레드만 우선순위가 있을 때 짧은 쓰레드가 계속 이어진다면 , 긴 쓰레드는 기아 상태에 빠짐
4. 데드락 : 여러 스레드가 서로가 끝나기를 기다리는 경우. 어느 쪽도 진행이 안됨.
5. 라이브락 : 락을 거는 단계에서 각 스레드가 서로를 방해하는 경유. 스레드는 계속해서 진행하려 하지만, 공명으로 인해 오랫동안 혹은 영원히 진행 불가능

실행모델
1. 생산자-소비자

  • 하나 이상의 생산자 스레드가 정보를 생성해 버퍼나 대기열(한정된 자원)에 넣고, 하나 이상의 소비자 스레드가 대기열(한정된 자원)에서 정보를 가져와 사용하는 경우.
  • 이 때 생산자와 소비자는 서로 시그널을 주도 받으며 작업을 진행 하는데, 잘못하면 둘 다 진행 가능함에도 불구하고 서로 기다리고 있는 경우가 발생할 수도 있음.
  1. 읽기-쓰기
  • 읽기 쓰레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 쓰레드가 공유 자원을 이따끔 갱신하는 경우.
  • 이럴 경우, 처리율, throughput갱신 의 비율을 잘 고려해야함.
  • 만약 처리율을 강조한다면, 기아 현상이 생기거나 오래된 정보가 쌓임
  • 갱신을 강조한다면, 여러 읽기 쓰레드들이 기다리느라 지쳐버림..
  • 간단한 전략은, 읽기 쓰레드가 없을 때 까지 갱신을 우너하는 쓰기 쓰레드가 버퍼를 기다리는 방법.
  • 이 둘 사이의 균형을 잘 잡아야 함.
  1. 식사하는 철학자들
  • 철학자들이 둥근 식탁에서 식사를 하고 있음.
  • 배가 고프면 포크를 양 손에 쥐고 먹고, 배가 고프지 않으면 생각.
  • 각 철학자의 왼쪽에 포크가 놓여 있음.
  • 쓰레드와 자원으로 바꿔서 생각을 해보면, 여러 프로세서가 자원을 얻으려 경쟁하는 모습과 같음

대다수의 쓰레드 문제는 이 3가지 중 하나의 경우이기 때문에, 이러한 기본 알고리즘을 공부해보고 구현해 보는 것이 좋습니다.

동기화 하는 메서드 사이에 존재하는 의존성을 이해하라.

자바 언어에은 개별 메서드를 보호하는 synchronized라는 개념이 있지만, 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지 반드시 확인해야 합니다.

공유 객체 하나에는 메서드 하나만 사용할 것

하지만, 공유 객체 하나에 여러 메서드가 필요한 상황도 생깁니다. 그럴 때는 세 가지 방법이 있습니다.

  1. 클라이언에서 잠금 - 클라이언트에서 첫 번쨰 메서드를 호출하기 전에 서버를 잠금. 마지막 메서드를 호출할 때 까지 잠금을 유지
  2. 서버에서 잠금 - 서버에서 “서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제” 하는 메서드 구현. 클라이언트는 이 메서드 호출
  3. 연결서버 - 잠금을 수행하는 중간 단계 생성.

동기화 하는 부분을 작게 만들어라.

synchronized를 사용하면 이 걸립니다. 따라서, 락이 많아진다면 부하가 커지겠죠. 이러한 영역을 최소화 시켜야 합니다.

올바른 종료 코드는 구현하기 어렵다.

데드락과 같이, 돌아가는 시스템을 구현하는 것과 잘 종료하는 프로그램을 구현하는 것은 다른 이야기라고 합니다.

종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라

스레드 코드 테스트하기.

코드가 올바르다고 증명하기는 불가능 하고, 테스트가 정확성을 보장하지는 않지만, 그럼에도 충분한 테스트는 위험을 낮춥니다.

따라서,

문제를 노출하는 테스트 케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대 안된다.

따라서, 본문은 세가지 지침을 소개합니다.

  1. 말도 안 되는 실패는 잠정적인 스레드 문제로 취급하라.
  • 맨 처음 봤던 것 처러므, 다중 스레드 코드는 종종 “말도 안되는” 오류를 일으키는데, 이는 직관적으로 이해하기가 어렵습니다. 엄청나게 많은 경로중 일부에서 발생하기 떄문입니다. 그러다 보니, 실패를 재현하기도 어렵고, 다시 돌렸더니 되더라- 하는 식으로 무시하는 경우가 많습니다.
  • 그럴 경우, 잘못된 코드 위에 코드가 계속 쌓일 수 있습니다.

2, 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.

  • 당연하죠. 스레드가 호출하는 POJO를 먼저 잘 만들어야 합니다. 스레드 환경 밖에서 코드를 올바로 돌리고, 스레드 밖의 버그와 스레드 환경에서의 버그를 동시에 디버깅 하면 안됩니다.
    3.다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라.
  • 한 쓰레드로 실행하거나, 여러 쓰레드로 실행하거나, 실행 중 스레드 수를 바꿔본다.
  • 스레드 코드를 실제 완경이나 테스트 환경에서 돌려본다.
  • 테스트 코드를 빨리, 천천히, 다양한 속도로 도려본다.
  • 반복 테스트가 가능하도록 테스트케이스를 작성한다.
  1. 다중 쓰레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작서앟라.

  2. 프로세서 수보다 많은 쓰레드를 돌려보라.

  • 시스템이 스와핑을 할 떄도 문제가 발생하는데, 스와핑을 일으키려면 프로세서 수보다 많은 스레드를 돌려봐야 합니다.
  1. 다른 플랫폼에서 돌려보라

  2. 코드에 보조 코드를 넣어 돌려라

  • 직접 구현하기
    : wait, sleep, yield, priority등의 함수를 직접 추가. 특별히 까다로운 코드를 테스트 하는 경우에 적합
    -> 문제점? 보조 코드를 삽입할 적정 위치 직접 찾아야 함, 어떤 함수를 어디서 호출해야 적당할까, 배포 환경에 보조 코드 그대로 남겨두면 성능 저하, 무작위성으로 인한 확률의 문제!
  • 자동화
    : AOF, CGLIB<ASM 등.
profile
SKKU Humanities & Computer Science

0개의 댓글