다른 개발자가 작업을 완료 한 후 메소드를 호출하도록 강제


34

Java 7의 라이브러리에는 다른 클래스에 서비스를 제공하는 클래스가 있습니다. 이 서비스 클래스의 인스턴스를 생성 한 후 그 중 하나의 메소드를 여러 번 호출 할 수 있습니다 (메소드라고 부릅니다 doWork()). 그래서 서비스 클래스의 작업이 언제 완료되는지 모르겠습니다.

문제는 서비스 클래스가 무거운 객체를 사용하고 해제해야한다는 것입니다. 이 부분을 메소드로 설정 release()했지만 ( 호출하자 ) 다른 개발자가이 메소드를 사용한다고 보장하지는 않습니다.

서비스 클래스 작업을 완료 한 후 다른 개발자가이 메소드를 호출하도록 할 수있는 방법이 있습니까? 물론 나는 그것을 문서화 할 수는 있지만 강제로하고 싶습니다.

참고 : 다음에 호출 할 때 해당 객체가 필요 하기 때문에 release()메소드에서 메소드를 호출 할 수 없습니다 .doWork()doWork()


46
당신이하고있는 일은 시간적 결합의 형태이며 일반적으로 코드 냄새로 간주됩니다. 대신 더 나은 디자인을 만들어내는 것이 좋습니다.
Frank

1
@Frank 기본 레이어의 디자인을 변경하는 것이 옵션이라면 이것이 가장 좋은 방법이라고 동의합니다. 그러나 나는 이것이 다른 사람의 서비스를 둘러싼 래퍼라고 생각합니다.
Dar Brett

서비스에 어떤 종류의 무거운 물체가 필요합니까? 서비스 클래스가 (시간 결합없이) 자체 리소스를 자동으로 관리 할 수없는 경우 해당 리소스를 다른 곳에서 관리하고 서비스에 주입해야합니다. 서비스 클래스에 대한 단위 테스트를 작성 했습니까?
에서 오는

18
그것을 요구하는 것은 매우 "비 자바"입니다. Java는 가비지 콜렉션이 포함 된 언어이므로 이제 개발자가 "정리"해야하는 일종의 중단이 발생합니다.
Pieter B

22
@PieterB 왜? AutoCloseable이 정확한 목적을 위해 만들어졌습니다.
BgrWorker

답변:


48

실용적인 솔루션은 클래스를 만들고 백스톱 AutoCloseable으로 finalize()메소드를 제공하는 것입니다 (적절한 경우 ... 아래 참조). 그런 다음 클래스의 사용자가 리소스를 사용 하거나 close()명시 적으로 호출하도록 합니다.

물론 나는 그것을 문서화 할 수는 있지만 강제로하고 싶습니다.

불행하게도, 방법이 없습니다 (1) 옳은 일을하기 위해 프로그래머를 강제로 자바있다. 정적 코드 분석기에서 잘못된 사용법을 선택하는 것이 가장 좋습니다.


종료 자의 주제에 대해. 이 Java 기능에는 유용한 사용 사례 가 거의 없습니다 . 마무리 도구를 사용하여 정리하는 경우 정리 작업을 수행하는 데 시간이 오래 걸릴 수있는 문제가 발생합니다. 종료 자는 GC가 객체에 더 이상 도달 할 수 없다고 결정한 후에 만 실행 됩니다. JVM이 전체 콜렉션을 수행 할 때까지 발생하지 않을 수 있습니다 .

따라서 해결하려는 문제가 조기 에 해제해야하는 자원을 회수하는 것이라면 종료를 사용하여 보트를 놓친 것입니다.

그리고 내가 위에서 말한 것을 얻지 못한 경우를 대비하여 ... 프로덕션 코드에서 종료자를 사용 하는 것은 거의 적합 하지 않으며 절대로 의존 해서는 안됩니다 !


1-방법이 있습니다. 사용자 코드에서 서비스 객체를 "숨기거나"라이프 사이클을 엄격하게 제어 할 준비가 된 경우 (예 : https://softwareengineering.stackexchange.com/a/345100/172 ) 사용자 코드는 호출 할 필요 가 없습니다 release(). 그러나 API는 더욱 복잡하고 제한적이며 "못생긴"IMO가됩니다. 또한 이것은 프로그래머가 올바른 일을하도록 강요 하지 않습니다 . 잘못된 일을하는 프로그래머의 능력을 제거하고 있습니다!


1
파이널 라이저는 매우 나쁜 교훈을 가르쳐줍니다. 나사를 조이면 해결해 드리겠습니다. netty의 접근 방식-> (ByteBuffer) 팩토리 가이 버퍼가 획득 된 위치를 인쇄하는 종료자를 사용하여 X % 바이트 버퍼 확률로 무작위로 생성하도록 지시하는 구성 매개 변수를 선호합니다 (아직 릴리스되지 않은 경우). 따라서 생산시이 값은 0 %로 설정 될 수 있습니다. 즉, 오버 헤드가없고 테스트 도중 제품을 출시하기 전에 누출을 감지하기 위해 50 % 또는 100 %와 같은 더 높은 값으로 설정할 수 있습니다.
Svetlin Zarev

11
객체 인스턴스가 가비지 수집되는 경우에도 종료자는 실행되지 않을 수 있습니다!
jwenting

3
AutoCloseable이 닫히지 않으면 Eclipse에서 컴파일러 경고가 발생합니다. 다른 Java IDE도 그렇게 할 것으로 기대합니다.
meriton-파업에

1
@jwenting 어떤 경우에 객체가 GC 일 때 실행되지 않습니까? 나는 그것을 진술하거나 설명하는 자료를 찾을 수 없었습니다.
Cedric Reichenbach

3
@Voo 당신은 내 의견을 오해했습니다. 두 가지 구현->이 DefaultResource있으며 FinalizableResource첫 번째 구현 을 확장하고 종료자를 추가합니다. 프로덕션에서는 DefaultResource파이널 라이저가없는 오버 헤드가없는 것을 사용 합니다. 개발 및 테스트 중에는 종료자가있는 앱을 사용하도록 구성 FinalizableResource하므로 오버 헤드가 추가됩니다. 개발 및 테스트 중에는 문제가되지 않지만 생산에 도달하기 전에 누출을 식별하고 수정하는 데 도움이 될 수 있습니다. 그러나 누출이 PRD에 도달하면 누출을 식별하기 위해 X % FinalizableResource를 생성하도록 공장을 구성 할 수 있습니다.
Svetlin Zarev

55

이것은 AutoCloseable인터페이스에 대한 유스 케이스 와 Java 7에 도입 된 try-with-resources명령문 처럼 보입니다.

public class MyService implements AutoCloseable {
    public void doWork() {
        // ...
    }

    @Override
    public void close() {
        // release resources
    }
}

소비자는 다음과 같이 사용할 수 있습니다.

public class MyConsumer {
    public void foo() {
        try (MyService myService = new MyService()) {
            //...
            myService.doWork()
            //...
            myService.doWork()
            //...
        }
    }
}

release코드에서 예외가 발생하는지 여부에 관계없이 명시 적 호출에 대해 걱정할 필요가 없습니다 .


추가 답변

당신이 정말로 찾고있는 것이 아무도 당신의 기능을 잊어 버릴 수 없도록하는 것이라면 몇 가지 일을해야합니다.

  1. 의 모든 생성자 MyService가 비공개 인지 확인하십시오 .
  2. MyService이후에 정리할 수 있도록 단일 진입 점을 정의하십시오 .

당신은 같은 것을 할 것입니다

public interface MyServiceConsumer {
    void accept(MyService value);
}

public class MyService implements AutoCloseable {
    private MyService() {
    }


    public static void useMyService(MyServiceConsumer consumer) {
        try (MyService myService = new MyService()) {
            consumer.accept(myService)
        }
    }
}

그런 다음 다음과 같이 사용하십시오.

public class MyConsumer {
    public void foo() {
        MyService.useMyService(new MyServiceConsumer() {
            public void accept(MyService myService) {
                myService.doWork()
            }
        });
    }
}

Java-8 람다와 기능적 인터페이스 (이되는 곳) MyServiceConsumer가 없으면 무시할 수 있고 Consumer<MyService>소비자에게 상당히 영향을 미치기 때문에이 경로를 원래 권장하지 않았습니다 . 그러나 당신이 원하는 것이 전화를 release()받아야한다면 그것은 효과가 있습니다.


1
이 대답은 아마 내 것보다 낫습니다. 가비지 콜렉터가 시작되기 전에 내 길에 지연이 있습니다.
Dar Brett

1
클라이언트가를 사용 try-with-resources하지 않고 전화를 생략 try하여 close전화를 받지 못하게 하려면 어떻게해야 합니까?
Frank

@walpen이 방법은 같은 문제가 있습니다. 사용자가 사용하도록하려면 try-with-resources어떻게해야합니까?
hasanghaforian

1
@Frank / @hasanghaforian, 그렇습니다. 이러한 방식은 강제로 호출되지 않습니다. 친숙한 이점이 있습니다. Java 개발자 .close()는 클래스가 구현 될 때 반드시 호출 해야한다는 것을 알고 있습니다 AutoCloseable.
walpen

1
using결정 론적 마무리를 위해 종료 자 와 블록을 연결할 수 없습니까? 적어도 C #에서는 결정적인 마무리가 필요한 리소스를 활용할 때 개발자가 사용하는 습관이 매우 분명합니다. 누군가가 무언가를하도록 강요 할 수없는 경우, 쉬운 습관과 습관 형성이 차선책입니다. stackoverflow.com/a/2016311/84206
AaronLS 2016 년

52

그래 넌 할수있어

그것들을 절대로 강제로 해제하고 새 Service인스턴스 를 생성하는 것을 불가능하게함으로써 100 % 잊을 수 없게 만듭니다 .

그들이 전에 이와 같은 것을 한 경우 :

Service s = s.new();
try {
  s.doWork("something");
  if (...) s.doWork("something else");
  ...
}
finally {
  s.release(); // oops: easy to forget
}

그런 다음이를 변경해야합니다.

// Their code

Service.runWithRelease(new ServiceConsumer() {
  void run(Service s) {
    s.doWork("something");
    if (...) s.doWork("something else");
    ...
  }  // yay! no s.release() required.
}

// Your code

interface ServiceConsumer {
  void run(Service s);
}

class Service {

   private Service() { ... }      // now: private
   private void release() { ... } // now: private
   public void doWork() { ... }   // unchanged

   public static void runWithRelease(ServiceConsumer consumer) {
      Service s = new Service();
      try {
        consumer.run(s);
      }
      finally {
        s.release();
      } 
    } 
  }

주의 사항 :

  • 이 의사 코드를 생각해보십시오 .Java를 작성한 이후로 오래되었습니다.
  • 요즘에는 AutoCloseable누군가 언급 한 인터페이스를 포함하여 더 우아한 변형이있을 수 있습니다 . 그러나 주어진 예제는 즉시 사용할 수 있어야하며 우아함 (좋은 목표)을 제외하고는 변경해야 할 주요 이유가 없어야합니다. 이것은 AutoCloseable개인 구현 내부에서 사용할 수 있음을 의미합니다 . 사용자가 사용하는 것보다 내 솔루션의 장점은 AutoCloseable다시 잊을 수 없다는 것입니다.
  • new Service호출에 인수를 삽입하는 등 필요에 따라이를 제거해야합니다 .
  • 의견에서 언급했듯이, 이와 같은 구조 (즉,를 만들고 파괴하는 과정을 취하는 Service)가 호출자의 손에 속 하는지 여부에 대한 질문 은이 답변의 범위를 벗어납니다. 그러나 이것이 절대적으로 필요하다고 결정하면 이것이 가능합니다.
  • 한 의견 제안자가 예외 처리에 관한 다음 정보를 제공했습니다. Google 도서

2
downvote에 대한 의견이 기쁘므로 답변을 개선 할 수 있습니다.
AnoE

2
나는 공감하지 않았지만이 디자인은 가치보다 더 문제가 될 수 있습니다. 많은 복잡성을 추가하고 발신자에게 큰 부담을줍니다. 개발자는 클래스를 사용하여 적절한 인터페이스를 구현할 때마다이를 사용하는 것이 리소스를 사용하는 try-with-resources 스타일 클래스에 익숙해야하므로 일반적으로 충분합니다.
jpmc26

30
@ jpmc26, 재미있게도 그 접근 방식 발신자 가 리소스 수명주기에 대해 전혀 생각하지 않아도 복잡성과 부담을 입니다. 그 외에도 OP는 "사용자를 강제로해야합니까 ..."라고 묻지 않고 "사용자를 어떻게 강제해야합니까?" 그리고 그것이 충분히 중요하다면, 이것은 매우 잘 작동하는 하나의 솔루션이며, 구조적으로 간단합니다 (폐쇄 / 실행 가능으로 포장). 람다 / 절차 / 폐쇄가있는 다른 언어로도 전송할 수 있습니다. 글쎄, 나는 그것을 판단하기 위해 그것을 SE 민주주의에 맡길 것이다. OP는 어쨌든 이미 결정했습니다. ;)
AnoE

14
다시 말하지만, @ jpmc26, 나는이 접근법이 단일 사용 사례에 맞는지 여부에 대해별로 관심이 없습니다. OP 는 그러한 것을 집행하는 방법을 묻고이 답변 은 그러한 것을 집행하는 방법을 알려줍니다 . 우선 그렇게하는 것이 적절한 지 결정하는 것은 아닙니다. 표시된 기술은 도구, 도구, 도구 도구에 유용한 도구라고 생각합니다.
AnoE

11
이것은 정답입니다. 본질적으로 홀인
더미들

11

명령 패턴 을 사용하려고 시도 할 수 있습니다 .

class MyServiceManager {

    public void execute(MyServiceTask tasks...) {
        // set up everyting
        MyService service = this.acquireService();
        // execute the submitted tasks
        foreach (Task task : tasks)
            task.executeWith(service);
        // cleanup yourself
        service.releaseResources();
    }

}

이를 통해 리소스 획득 및 릴리스를 완벽하게 제어 할 수 있습니다. 호출자는 서비스에 작업을 제출하기 만하며 사용자는 자원을 확보하고 정리해야합니다.

그러나 캐치가 있습니다. 발신자는 여전히 다음을 수행 할 수 있습니다.

MyServiceTask t1 = // some task
manager.execute(t1);
MyServiceTask t2 = // some task
manager.execute(t2);

그러나 문제가 발생할 때이 문제를 해결할 수 있습니다. 성능 문제가 있고 일부 발신자가이 작업을 수행하는 것을 발견하면 적절한 방법을 보여주고 문제를 해결하십시오.

MyServiceTask t1 = // some task
MyServiceTask t2 = // some task
manager.execute(t1, t2);

다른 작업에 종속 된 작업에 대한 약속을 구현하여이 작업을 임의로 복잡하게 만들 수 있지만 배포하는 작업도 더 복잡해집니다. 이것은 시작일뿐입니다.

비동기

주석에서 지적했듯이 위의 내용은 실제로 비동기 요청에서는 작동하지 않습니다. 맞습니다. 그러나 이것은 CompleteableFuture를 사용하여 Java 8에서 쉽게 해결할 수 있습니다 . 특히 CompleteableFuture#supplyAsync개별 미래를 만들고 모든 작업이 완료 CompleteableFuture#allOf되면 리소스를 공개 합니다. 또는 Threads 또는 ExecutorServices를 사용하여 자신의 Futures / Promises 구현을 롤백 할 수도 있습니다.


1
다운 보터가 왜 이것이 나쁘다고 생각하는지에 대한 의견을 남기면 감사하겠습니다. 따라서 그러한 우려를 직접적으로 다루거나 적어도 미래에 대한 다른 견해를 얻을 수 있습니다. 감사!
Polygnome

공감 +1 이것은 접근 방식 일 수 있지만 서비스는 비동기 적이며 소비자는 다음 서비스를 요청하기 전에 첫 번째 결과를 얻어야합니다.
hasanghaforian

1
이것은 단순한 템플릿입니다. JS는 약속 으로이 문제를 해결합니다. 모든 작업이 완료되고 정리 될 때 호출되는 콜렉터 및 미래를 사용하여 Java에서 비슷한 작업을 수행 할 수 있습니다. 비 동기화되고 의존성을 얻는 것이 가능하지만 문제가 복잡합니다.
Polygnome

3

가능하면 사용자가 자원을 작성하지 못하게하십시오. 사용자가 메소드에 방문자 오브젝트를 전달하면 자원이 작성되고이를 방문자 오브젝트의 메소드에 전달한 후 해제됩니다.


답장을 보내 주셔서 감사하지만 실례합니다. 소비자에게 리소스를 전달한 다음 서비스를 요청할 때 다시받는 것이 더 낫다는 것을 의미합니까? 그런 다음 문제는 계속 될 것입니다. 소비자는 해당 리소스를 해제하는 것을 잊어 버릴 수 있습니다.
hasanghaforian

리소스를 제어하려면 리소스를 포기하지 말고 다른 사람의 실행 흐름에 완전히 전달하지 마십시오. 이를 수행하는 한 가지 방법은 사용자의 방문자 오브젝트에 대해 사전 정의 된 메소드를 실행하는 것입니다. AutoCloseable무대 뒤에서 매우 비슷한 일을합니다. 다른 각도에서 의존성 주입 과 유사합니다 .
9000

1

내 생각은 Polygnome과 비슷했습니다.

클래스에서 "do_something ()"메소드를 사용하여 개인 명령 목록에 명령을 추가하기 만하면됩니다. 그런 다음 실제로 작업을 수행하고 "release ()"를 호출하는 "commit ()"메서드를 갖습니다.

따라서 사용자가 commit ()을 호출하지 않으면 작업이 완료되지 않습니다. 그렇게하면 리소스가 해제됩니다.

public class Foo
{
  private ArrayList<ICommand> _commands;

  public Foo doSomething(int arg) { _commands.Add(new DoSomethingCommand(arg)); return this; }
  public Foo doSomethingElse() { _commands.Add(new DoSomethingElseCommand()); return this; }

  public void commit() { 
     for(ICommand c : _commands) c.doWork();
     release();
     _commands.clear();
  }

}

그런 다음 foo.doSomething.doSomethineElse (). commit ()과 같이 사용할 수 있습니다.


이것은 동일한 솔루션이지만 호출자에게 작업 목록을 유지하도록 요청하는 대신 내부적으로 유지 관리합니다. 핵심 개념은 문자 그대로 동일합니다. 그러나 이것은 내가 본 Java의 코딩 규칙을 따르지 않으며 실제로 읽기가 어렵습니다 (루프 헤더와 같은 줄의 루프 본문, 처음에는 _).
Polygnome

예, 명령 패턴입니다. 가능한 한 간결한 방법으로 생각하고있는 것에 대한 요점을 제공하기 위해 코드를 내려 놓았습니다. 개인 변수 앞에 종종 _ 접두사가 붙는 C #을 작성합니다.
Sava B.

-1

언급 된 것의 또 다른 대안은 가비지 수집기가 수동으로 리소스를 해제하지 않고 자동으로 정리할 수있는 방식으로 캡슐화 된 리소스를 관리하기 위해 "스마트 참조"를 사용하는 것입니다. 그것들의 유용성은 물론 구현에 달려 있습니다. 이 멋진 블로그 게시물 에서이 주제에 대해 자세히 읽으십시오 : https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references


이것은 흥미로운 아이디어이지만 약한 참조를 사용하여 OP의 문제를 어떻게 해결하는지 알 수 없습니다. 서비스 클래스는 다른 doWork () 요청을 받을지 여부를 모릅니다. 무거운 객체에 대한 참조를 해제 한 후 다른 doWork () 호출을 받으면 실패합니다.
Jay Elston

-4

다른 클래스 지향 언어의 경우 소멸자에 배치한다고 말하지만 옵션이 아니기 때문에 finalize 메서드를 재정의 할 수 있다고 생각합니다. 이렇게하면 인스턴스가 범위를 벗어나고 가비지 수집 될 때 해제됩니다.

@Override
public void finalize() {
    this.release();
}

Java에 익숙하지 않지만 이것이 finalize ()의 목적이라고 생각합니다.

http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#finalize ()


7
이것은 Stephen이 그의 답변에서 말한 이유에 대한 문제에 대한 나쁜 해결책입니다. –If you rely on finalizers to tidy up, you run into the problem that it can take a very long time for the tidy-up to happen. The finalizer will only be run after the GC decides that the object is no longer reachable. That may not happen until the JVM does a full collection.
Daenyth

4
그리고 그때조차도 파이널 라이저가 실제로 실행되지 않을 수도 있습니다 ...
jwenting

3
파이널 라이저는 믿을 수 없을 정도로 악명 높습니다. 달리기를 할 경우 보장 수 없는 경우에는 달리거나 달리지 않을 수도 있습니다. Josh Bloch 와 같은 Java 아키텍트조차도 파이널 라이저는 끔찍한 아이디어라고 말합니다 (참고 : Effective Java (2nd Edition) ).

이러한 종류의 문제로 인해 결정 론적 파괴와 RAII로 C ++을 그리워하게됩니다.
Mark K Cowan
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.