글 작성자: 자바니또

메시지 전송(메시징)

JAVA에서 메시징은 C에서의 함수호출과 비슷하다.
메시징의 의미는 객체가 또 다른 객체의 인터페이스를 통해 어떠한 행위를 하라고 명령하는 것으로 필요하다면 데이터를 담아서 보낼 수도 있다.
여기서 말하는 인터페이스는 JAVA의 interface키워드를 말하는 것이 아니라 객체 간의 소통을 가능하게 해주는 public method를 뜻한다.

왜 메시징을 보내야 할까?

한 클래스 안에서 다 해결하면 안 되는 것인가? 물론 다들 답을 알고 있을 것이다. SOLID의 SRP원칙에 의해 하나의 클래스는 하나의 책임만 갖도록 설계해야 한다.
그렇기 때문에 자신이 못하는 것을 누군가 대신 해주 길 원할 때, 그것을 해줄 수 있는 객체에게 메시징을 하는 것이다.
덧붙이자면 이것을 행위의 책임을 위임했다 해서 위임이라 하는데, 이것들에 대해서는 다음에 자세히 다루기로 한다.

dog.drink(water);    //수신자.오퍼레이션명(인자)

메시지는 오퍼레이션명+인자, 메시지 전송(메시징)은 메시지에 수신자가 더해진 형태이다.
메시지는 명령이기 때문에 오퍼레이션명 또한 명령문으로 작성하는 것이 좋다.
오버로딩의 가능성이 있다면 drinkWater()가 아니라 drink(water) 처럼 목적어를 빼는 것도 생각해볼 수 있다.
인자의 타입으로 충분히 알 수 있고 이렇게 함으로써 좀더 추상적인 이름이 되어 코드의 유연성을 높일 수 있기 때문이다.

좀 더 범용적으로 표현하자면 인터페이스의 오퍼레이션 명은 어떻게 수행하는지 알려주는 구체적인 이름보단 무엇을 하는지 행위만 간략히 표현하는 추상적인 이름으로 짓는 것이 좋다.

여기서 한가지 궁금할 수도 있다. 메시징메서드 호출은 같은 것인가? 엄밀히 말하면 다르다.
비교하기에 앞서 메시징을 보내는 객체를 송신객체 또는 클라이언트 객체, 받는 객체를 수신객체 또는 서버 객체라고 한다는 것을 알아두고 아래의 그림을 보자.

클라이언트 객체가 메시지를 보내면 Java Runtime System이 메시지 전송을 오퍼레이션 호출로 해석하여 서버 객체에서 오퍼레이션에 맞는 적절한 메서드를 찾아 실행한다.
메시징은 객체의 구체적인 메서드를 부르는 것이 아닌 인터페이스를 통해 추상적인 명령을 전달하는 것이고 메서드 호출은 Runtime에 오퍼레이션 호출(메시징)에 의해 실제로 구현된 코드를 실행하는 것이다.

    public interface Puppy{
        public void drink(Water water);
    }
    public class Client{
        public void Client(Puppy puppy, Water water){
            puppy.drink(water);
        }
    }

Puppy의 drink()는 구현되지 않았지만 컴파일 에러가 나지 않는 이유는 구현부가 없어도 메시징은 보낼수 있기 때문이다.

이렇게 메시징을 기반으로 코딩을 하면 다음의 이점이 있다.

  1. 메시지 전송자는 수신자가 실행하는 메서드가 어떻게 실행되는지 몰라도 된다. 단지 인터페이스의 이름만으로 무엇을 할 수 있는지 알 수 있고 명령을 내리면 된다.
  2. 메시지 수신자는 누가 전송하는지 알 필요없고 전송받은 메시지에 대응하여 자체적인 처리를 결정 할 수 있는 자율권을 얻는다.
  3. 메시지 전송자와 수신자가 느슨하게 결합되도록 할 수 있다.

메서드가 어떻게 실행되는지 모르는 것이 왜 이득인가?

우리가 사용하는 스마트폰을 생각해보자. 우리는 키를 하나하나 입력할 때마다 어떤 내부로직이 실행되어 결과가 나오는지를 알아야하는가? 그렇지 않다.
우리는 UI라고 하는 사용자 인터페이스를 통해 무엇을 할 수있는지 알 수 있고 버튼을 누르기만 하면 된다. 코드를 통해 더 자세히 알아보자.

public interface Puppy{
    public void drink(Water water);
}
public class Client{
    public void Client(Puppy puppy, Water water){
        puppy.drink(water);
    }
}

Client가 생성되면 전달받은 Puppy와 Water를 사용하여 drink()를 실행하는 간단한 예시이다.

우리는 Puppy가 Choco인지 Berry인지 아는가?

모른다. 단지 전달받은 것은 Puppy라는 것과 Puppy의 계약책임에 의해 분명히 drink()라는 인터페이스를 가지고 있다는 것만 알 수 있다.
물론 코드를 파헤쳐서 Client를 생성할 때 전달되는 인자를 확인할 수 있다.
하지만 굳이 그러지 않아도 우리는 오퍼레이션명과 인자를 통해 메시지의 의미를 알 수있다.
puppy가 drink라는 명령을 어떻게 수행하는지는 알 필요 없다. 그저 메시지를 보내면 우리는 어떤 일이 벌어질지 예상 할 수 있다. API문서나 주석을 단다면 확실히 알 수 있다.

UI의 버튼을 누르듯이 클라이언트 객체는 필요한 것이 있다면 그것을 처리할 수 있는 서버객체에게 메시지만 전송하면 그 이후는 서버객체에게 달려있다.


메시지에 대응하여 자체적인 처리를 결정 할 수 있는 자율권을 얻는다.

public interface Puppy{
    public void drink(Water water);
}
public class Client{
    public void Client(Puppy puppy, Water water){
        puppy.drink(water);
    }
}

메시지 전송 이후는 서버 객체에게 달려 있다고 했다. 현재 클라이언트 객체는 Choco와 Berry가 어떻게 구현했는지 알 수 없다.
Choco는 자신이 가진 초콜릿을 넣어 먹을 수도 있고, 물을 마신 후에 Berry에게도 마시라고 메시지를 보낼 수도 있다.

서버객체는 메시지를 받으면 메시지에 대응하여 자신이 가지고 있는 데이터를 이용하여 어떻게 처리 할지를 결정할 수 있는 자율권을 얻는다. 참고로 말하자면, 이러한 모델을 ActorModel(행위자 모델)이라고 한다.


메시지 전송자와 수신자가 느슨하게 결합되도록 할 수 있다.

public interface Puppy{
    public void drink(Water water);
}
public class Choco implements Puppy{
    @Override
    public void drink(Water water){;}
    public void bark(){;}
}
public class Client{
    public void Client(Choco choco, Water water){
        choco.drink(water);
        choco.bark();
    }
}

코드를 살짝 바꿔보았다. Client가 추상클래스가 아니라 구체클래스인 Choco를 인자로 받아 처리하고 있다.
이 코드는 Client는 Choco의 변화에 매우 민감해질 수 밖에 없다. Choco라는 클래스가 사라질 수도 있고 이름이 변경될 수도 있다.
또 bark()의 오퍼레이션명을 변경하거나 삭제할 수도 있다.

이렇게 변화에 민감해질수록 강한 결합이라고 하고, 그 반대를 느슨한 결합이라 한다.


다형성

public interface Puppy{
    public void drink(Water water);
}
public class Client{
    public void doAction(Puppy puppy, Water water){
        puppy.drink(water);
    }
}

다형성은 유연한 코드의 끝판왕이다. 위의 코드는 우리가 누르는 버튼에 따라 Client에게 doAction()메시지의 인자를 바꿔서 메시징하는 코드의 일부이다.
우리가 '1'을 누르면 Choco객체를 인자로 메시징하고, '2'를 누르면 Berry객체를 인자로 메시징한다고 하자.
puppy.drink(water)라는 메시징은 컴파일시에 고정되고 변하지 않는다. 하지만 Runtime에 우리가 누르는 버튼에 따라 주입되는 Puppy가 바뀌고 결과 또한 다르게 나올 것이다.

즉, 다형성이란 같은 메시징에 대하여 서로 다른 처리를 할 수있는 것을 말한다_. 코드의 관점으로 보자면 한가지의 코드로 여러 기능을 할 수 있는 것을 말한다.
또 개발자의 관점에서 보자면 컴파일 시점의 고정된 의존관계를 런타임에 동적으로 바꿀 수 있다는 것이다.

우리가 키보드 자판의 같은 키를 누르는데 앱마다 다른 기능을 하는 것도 다형성이다.
다형성을 이루려면 구현객체를 직접 의존하지 않아야 한다.
잘 변하지 않는 추상 클래스에 의존함으로써 구현 객체와 Runtime시에 결합하는 아주 느슨한 결합을 만들어라.


결론

  • 클라이언트 객체는 원하는 것이 있다면 그것을 할 수 있는 서버객체의 인터페이스를 통해 메시징하면 된다.
  • 서버객체는 누가 메시지를 보냈는지 고려할 필요 없다. 단지 메시지를 받았을 때의 처리만을 메서드로 만들어 놓으면 된다.
  • 우린 앞으로 puppy.drink(water)를 puppy의 drink메서드 호출이 아니라 puppy에게 메시징이라 하자.
  • 다형성은 유연한 코드의 끝판왕으로써 컴파일 시점의 고정된 의존관계를 런타임에 동적으로 바꿀 수 있게 해준다.